Native rewrite #878

Merged
akinwale merged 65 commits from native-rewrite into master 2020-05-23 08:49:00 +02:00
17 changed files with 380 additions and 14 deletions
Showing only changes of commit 5eb35e3283 - Show all commits

View file

@ -823,7 +823,7 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener
closeIcon.setVisibility(visible ? View.VISIBLE : View.GONE);
}
private int getScaledValue(int value) {
public int getScaledValue(int value) {
float scale = getResources().getDisplayMetrics().density;
return (int) (value * scale + 0.5f);
}

View file

@ -22,6 +22,7 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import io.lbry.browser.MainActivity;
import io.lbry.browser.R;
import io.lbry.browser.listener.SelectionModeListener;
import io.lbry.browser.model.Claim;
@ -54,6 +55,7 @@ public class ClaimListAdapter extends RecyclerView.Adapter<ClaimListAdapter.View
private boolean inSelectionMode;
@Setter
private SelectionModeListener selectionModeListener;
private float scale;
public ClaimListAdapter(List<Claim> items, Context context) {
this.context = context;
@ -63,6 +65,9 @@ public class ClaimListAdapter extends RecyclerView.Adapter<ClaimListAdapter.View
quickClaimUrlMap = new HashMap<>();
notFoundClaimIdMap = new HashMap<>();
notFoundClaimUrlMap = new HashMap<>();
if (context != null) {
scale = context.getResources().getDisplayMetrics().density;
}
}
public List<Claim> getSelectedItems() {
@ -87,6 +92,10 @@ public class ClaimListAdapter extends RecyclerView.Adapter<ClaimListAdapter.View
return null;
}
public List<Claim> getItems() {
return new ArrayList<>(this.items);
}
public void clearItems() {
clearSelectedItems();
this.items.clear();
@ -122,6 +131,11 @@ public class ClaimListAdapter extends RecyclerView.Adapter<ClaimListAdapter.View
notifyDataSetChanged();
}
public void removeItems(List<Claim> claims) {
items.removeAll(claims);
notifyDataSetChanged();
}
public void removeItem(Claim claim) {
items.remove(claim);
selectedItems.remove(claim);
@ -261,9 +275,18 @@ public class ClaimListAdapter extends RecyclerView.Adapter<ClaimListAdapter.View
return new ClaimListAdapter.ViewHolder(v);
}
public int getScaledValue(int value) {
return (int) (value * scale + 0.5f);
}
@Override
public void onBindViewHolder(ClaimListAdapter.ViewHolder vh, int position) {
int type = getItemViewType(position);
int paddingTop = position == 0 ? 16 : 8;
int paddingBottom = position == getItemCount() - 1 ? 16 : 8;
int paddingTopScaled = getScaledValue(paddingTop);
int paddingBottomScaled = getScaledValue(paddingBottom);
vh.itemView.setPadding(vh.itemView.getPaddingLeft(), paddingTopScaled, vh.itemView.getPaddingRight(), paddingBottomScaled);
Claim original = items.get(position);
boolean isRepost = Claim.TYPE_REPOST.equalsIgnoreCase(original.getValueType());

View file

@ -24,6 +24,19 @@ public class User {
private String primaryEmail;
private String rewardStatusChangeTrigger;
private String updatedAt;
private List<String> youtubeChannels;
private List<YoutubeChannel> youtubeChannels;
private List<String> deviceTypes;
@Data
public static class YoutubeChannel {
String ytChannelName;
String lbryChannelName;
String channelClaimId;
String syncStatus;
String statusToken;
boolean transferable;
String transferState;
List<String> publishToAddress;
String publicKey;
}
}

View file

@ -0,0 +1,63 @@
package io.lbry.browser.tasks.claim;
import android.os.AsyncTask;
import android.view.View;
import org.json.JSONObject;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import io.lbry.browser.exceptions.ApiCallException;
import io.lbry.browser.tasks.GenericTaskHandler;
import io.lbry.browser.utils.Helper;
import io.lbry.browser.utils.Lbry;
public class AbandonChannelTask extends AsyncTask<Void, Void, Boolean> {
private List<String> claimIds;
private List<String> successfulClaimIds;
private List<String> failedClaimIds;
private List<Exception> failedExceptions;
private View progressView;
private AbandonHandler handler;
public AbandonChannelTask(List<String> claimIds, View progressView, AbandonHandler handler) {
this.claimIds = claimIds;
this.progressView = progressView;
this.handler = handler;
}
protected void onPreExecute() {
Helper.setViewVisibility(progressView, View.VISIBLE);
}
public Boolean doInBackground(Void... params) {
successfulClaimIds = new ArrayList<>();
failedClaimIds = new ArrayList<>();
failedExceptions = new ArrayList<>();
for (String claimId : claimIds) {
try {
Map<String, Object> options = new HashMap<>();
options.put("claim_id", claimId);
options.put("blocking", false);
JSONObject result = (JSONObject) Lbry.genericApiCall(Lbry.METHOD_CHANNEL_ABANDON, options);
successfulClaimIds.add(claimId);
} catch (ApiCallException ex) {
failedClaimIds.add(claimId);
failedExceptions.add(ex);
}
}
return true;
}
protected void onPostExecute(Boolean result) {
Helper.setViewVisibility(progressView, View.GONE);
if (handler != null) {
handler.onComplete(successfulClaimIds, failedClaimIds, failedExceptions);
}
}
}

View file

@ -0,0 +1,7 @@
package io.lbry.browser.tasks.claim;
import java.util.List;
public interface AbandonHandler {
void onComplete(List<String> successfulClaimIds, List<String> failedClaimIds, List<Exception> errors);
}

View file

@ -0,0 +1,32 @@
package io.lbry.browser.tasks.file;
import android.os.AsyncTask;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import io.lbry.browser.exceptions.ApiCallException;
import io.lbry.browser.tasks.GenericTaskHandler;
import io.lbry.browser.utils.Lbry;
// Just run delete on the specified claim IDs (no need for a handler)
public class BulkDeleteFilesTask extends AsyncTask<Void, Void, Boolean> {
private List<String> claimIds;
public BulkDeleteFilesTask(List<String> claimIds) {
this.claimIds = claimIds;
}
protected Boolean doInBackground(Void... params) {
for (String claimId : claimIds) {
try {
Map<String, Object> options = new HashMap<>();
options.put("claim_id", claimId);
options.put("delete_from_download_dir", true);
Lbry.genericApiCall(Lbry.METHOD_FILE_DELETE, options);
} catch (ApiCallException ex) {
// pass
}
}
return true;
}
}

View file

@ -26,6 +26,7 @@ public class DeleteFileTask extends AsyncTask<Void, Void, Boolean> {
options.put("delete_from_download_dir", true);
return (boolean) Lbry.genericApiCall(Lbry.METHOD_FILE_DELETE, options);
} catch (ApiCallException ex) {
error = ex;
return false;
}
}

View file

@ -2,6 +2,7 @@ package io.lbry.browser.ui.channel;
import android.content.Context;
import android.content.DialogInterface;
import android.graphics.Color;
import android.os.AsyncTask;
import android.os.Bundle;
import android.view.LayoutInflater;
@ -19,6 +20,7 @@ import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.google.android.material.snackbar.Snackbar;
import java.util.ArrayList;
import java.util.HashMap;
@ -32,6 +34,8 @@ import io.lbry.browser.listener.SdkStatusListener;
import io.lbry.browser.listener.SelectionModeListener;
import io.lbry.browser.model.Claim;
import io.lbry.browser.model.NavMenuItem;
import io.lbry.browser.tasks.claim.AbandonChannelTask;
import io.lbry.browser.tasks.claim.AbandonHandler;
import io.lbry.browser.tasks.claim.ClaimListResultHandler;
import io.lbry.browser.tasks.claim.ClaimListTask;
import io.lbry.browser.ui.BaseFragment;
@ -144,7 +148,7 @@ public class ChannelManagerFragment extends BaseFragment implements ActionMode.C
ClaimListTask task = new ClaimListTask(Claim.TYPE_CHANNEL, getLoading(), new ClaimListResultHandler() {
@Override
public void onSuccess(List<Claim> claims) {
Lbry.ownChannels = new ArrayList<>(claims);
Lbry.ownChannels = Helper.filterDeletedClaims(new ArrayList<>(claims));
Context context = getContext();
if (adapter == null) {
adapter = new ClaimListAdapter(claims, context);
@ -260,7 +264,7 @@ public class ChannelManagerFragment extends BaseFragment implements ActionMode.C
.setPositiveButton(R.string.yes, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
//handleDeleteSelectedClaims(selectedClaims);
handleDeleteSelectedClaims(selectedClaims);
}
}).setNegativeButton(R.string.no, null);
builder.show();
@ -270,4 +274,48 @@ public class ChannelManagerFragment extends BaseFragment implements ActionMode.C
return false;
}
private void handleDeleteSelectedClaims(List<Claim> selectedClaims) {
List<String> claimIds = new ArrayList<>();
for (Claim claim : selectedClaims) {
claimIds.add(claim.getClaimId());
}
if (actionMode != null) {
actionMode.finish();
}
Helper.setViewVisibility(channelList, View.INVISIBLE);
Helper.setViewVisibility(fabNewChannel, View.INVISIBLE);
AbandonChannelTask task = new AbandonChannelTask(claimIds, bigLoading, new AbandonHandler() {
@Override
public void onComplete(List<String> successfulClaimIds, List<String> failedClaimIds, List<Exception> errors) {
View root = getView();
if (root != null) {
if (failedClaimIds.size() > 0) {
Snackbar.make(root, R.string.one_or_more_channels_failed_abandon, Snackbar.LENGTH_LONG).
setBackgroundTint(Color.RED).setTextColor(Color.WHITE).show();
} else if (successfulClaimIds.size() == claimIds.size()) {
try {
String message = getResources().getQuantityString(R.plurals.channels_deleted, successfulClaimIds.size());
Snackbar.make(root, message, Snackbar.LENGTH_LONG).show();
} catch (IllegalStateException ex) {
// pass
}
}
}
Lbry.abandonedClaimIds.addAll(successfulClaimIds);
if (adapter != null) {
adapter.setItems(Helper.filterDeletedClaims(adapter.getItems()));
}
Helper.setViewVisibility(channelList, View.VISIBLE);
Helper.setViewVisibility(fabNewChannel, View.VISIBLE);
checkNoChannels();
}
});
task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
}

View file

@ -237,6 +237,7 @@ public class FileViewFragment extends BaseFragment implements
}
resetViewCount();
resetFee();
checkNewClaimAndUrl(newClaim, newUrl);
if (newClaim != null) {
@ -364,6 +365,7 @@ public class FileViewFragment extends BaseFragment implements
currentUrl = url;
logUrlEvent(url);
resetViewCount();
resetFee();
View root = getView();
if (root != null) {
((RecyclerView) root.findViewById(R.id.file_view_related_content_list)).setAdapter(null);
@ -431,6 +433,7 @@ public class FileViewFragment extends BaseFragment implements
public void openClaimUrl(String url) {
resetViewCount();
resetFee();
currentUrl = url;
ClaimCacheKey key = new ClaimCacheKey();
@ -508,6 +511,7 @@ public class FileViewFragment extends BaseFragment implements
View root = getView();
if (root != null) {
PlayerView view = root.findViewById(R.id.file_view_exoplayer_view);
view.setPlayer(null);
view.setPlayer(MainActivity.appPlayer);
}
}
@ -896,6 +900,10 @@ public class FileViewFragment extends BaseFragment implements
getView().findViewById(R.id.file_view_action_download).setVisibility(View.VISIBLE);
getView().findViewById(R.id.file_view_unsupported_container).setVisibility(View.GONE);
actionDelete.setEnabled(true);
claim.setFile(null);
Lbry.unsetFilesForCachedClaims(Arrays.asList(claim.getClaimId()));
restoreMainActionButton();
}
@ -1135,9 +1143,20 @@ public class FileViewFragment extends BaseFragment implements
}
private void resetViewCount() {
TextView textViewCount = getView().findViewById(R.id.file_view_view_count);
Helper.setViewText(textViewCount, null);
Helper.setViewVisibility(textViewCount, View.GONE);
View root = getView();
if (root != null) {
TextView textViewCount = root.findViewById(R.id.file_view_view_count);
Helper.setViewText(textViewCount, null);
Helper.setViewVisibility(textViewCount, View.GONE);
}
}
private void resetFee() {
View root = getView();
if (root != null) {
TextView feeView = root.findViewById(R.id.file_view_fee);
feeView.setText(null);
Helper.setViewVisibility(root.findViewById(R.id.file_view_fee_container), View.GONE);
}
}
private void loadViewCount() {

View file

@ -1,10 +1,13 @@
package io.lbry.browser.ui.library;
import android.content.Context;
import android.content.DialogInterface;
import android.graphics.Typeface;
import android.os.AsyncTask;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;
@ -12,16 +15,23 @@ import android.widget.ProgressBar;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.view.ActionMode;
import androidx.cardview.widget.CardView;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.google.android.material.snackbar.Snackbar;
import org.json.JSONException;
import org.json.JSONObject;
import java.nio.channels.AsynchronousChannel;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import io.lbry.browser.MainActivity;
import io.lbry.browser.R;
@ -29,22 +39,31 @@ import io.lbry.browser.adapter.ClaimListAdapter;
import io.lbry.browser.data.DatabaseHelper;
import io.lbry.browser.listener.DownloadActionListener;
import io.lbry.browser.listener.SdkStatusListener;
import io.lbry.browser.listener.SelectionModeListener;
import io.lbry.browser.model.Claim;
import io.lbry.browser.model.LbryFile;
import io.lbry.browser.model.NavMenuItem;
import io.lbry.browser.model.ViewHistory;
import io.lbry.browser.tasks.claim.AbandonChannelTask;
import io.lbry.browser.tasks.claim.AbandonHandler;
import io.lbry.browser.tasks.file.BulkDeleteFilesTask;
import io.lbry.browser.tasks.file.DeleteFileTask;
import io.lbry.browser.tasks.file.FileListTask;
import io.lbry.browser.tasks.localdata.FetchViewHistoryTask;
import io.lbry.browser.ui.BaseFragment;
import io.lbry.browser.ui.channel.ChannelFormFragment;
import io.lbry.browser.utils.Helper;
import io.lbry.browser.utils.Lbry;
import io.lbry.browser.utils.LbryAnalytics;
public class LibraryFragment extends BaseFragment implements DownloadActionListener, SdkStatusListener {
public class LibraryFragment extends BaseFragment implements
ActionMode.Callback, DownloadActionListener, SelectionModeListener, SdkStatusListener {
private static final int FILTER_DOWNLOADS = 1;
private static final int FILTER_HISTORY = 2;
private static final int PAGE_SIZE = 50;
private ActionMode actionMode;
private int currentFilter;
private List<LbryFile> currentFiles;
private View layoutSdkInitializing;
@ -244,6 +263,9 @@ public class LibraryFragment extends BaseFragment implements DownloadActionListe
currentFilter = FILTER_HISTORY;
linkFilterDownloads.setTypeface(null, Typeface.NORMAL);
linkFilterHistory.setTypeface(null, Typeface.BOLD);
if (actionMode != null) {
actionMode.finish();
}
if (contentListAdapter != null) {
contentListAdapter.clearItems();
contentListAdapter.setCanEnterSelectionMode(false);
@ -261,6 +283,7 @@ public class LibraryFragment extends BaseFragment implements DownloadActionListe
private void initContentListAdapter(List<Claim> claims) {
contentListAdapter = new ClaimListAdapter(claims, getContext());
contentListAdapter.setCanEnterSelectionMode(true);
contentListAdapter.setSelectionModeListener(this);
contentListAdapter.setListener(new ClaimListAdapter.ClaimListItemListener() {
@Override
public void onClaimClicked(Claim claim) {
@ -459,4 +482,104 @@ public class LibraryFragment extends BaseFragment implements DownloadActionListe
currentFilter == FILTER_HISTORY ?
View.GONE : View.VISIBLE);
}
@Override
public boolean onCreateActionMode(ActionMode actionMode, Menu menu) {
this.actionMode = actionMode;
Context context = getContext();
if (context instanceof MainActivity) {
MainActivity activity = (MainActivity) context;
if (!activity.isDarkMode()) {
activity.getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_VISIBLE);
}
}
actionMode.getMenuInflater().inflate(R.menu.menu_claim_list, menu);
return true;
}
@Override
public void onDestroyActionMode(ActionMode actionMode) {
if (contentListAdapter != null) {
contentListAdapter.clearSelectedItems();
contentListAdapter.setInSelectionMode(false);
contentListAdapter.notifyDataSetChanged();
}
Context context = getContext();
if (context != null) {
MainActivity activity = (MainActivity) context;
if (!activity.isDarkMode()) {
activity.getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);
}
}
this.actionMode = null;
}
@Override
public boolean onPrepareActionMode(androidx.appcompat.view.ActionMode actionMode, Menu menu) {
menu.findItem(R.id.action_edit).setVisible(false);
return true;
}
@Override
public boolean onActionItemClicked(androidx.appcompat.view.ActionMode actionMode, MenuItem menuItem) {
if (R.id.action_delete == menuItem.getItemId()) {
if (contentListAdapter != null && contentListAdapter.getSelectedCount() > 0) {
final List<Claim> selectedClaims = new ArrayList<>(contentListAdapter.getSelectedItems());
String message = getResources().getQuantityString(R.plurals.confirm_delete_files, selectedClaims.size());
AlertDialog.Builder builder = new AlertDialog.Builder(getContext()).
setTitle(R.string.delete_selection).
setMessage(message)
.setPositiveButton(R.string.yes, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
handleDeleteSelectedClaims(selectedClaims);
}
}).setNegativeButton(R.string.no, null);
builder.show();
return true;
}
}
return false;
}
private void handleDeleteSelectedClaims(List<Claim> selectedClaims) {
List<String> claimIds = new ArrayList<>();
for (Claim claim : selectedClaims) {
claimIds.add(claim.getClaimId());
}
new BulkDeleteFilesTask(claimIds).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
Lbry.unsetFilesForCachedClaims(claimIds);
if (currentFilter == FILTER_DOWNLOADS) {
contentListAdapter.removeItems(selectedClaims);
}
if (actionMode != null) {
actionMode.finish();
}
View root = getView();
if (root != null) {
String message = getResources().getQuantityString(R.plurals.files_deleted, claimIds.size());
Snackbar.make(root, message, Snackbar.LENGTH_LONG).show();
}
}
public void onEnterSelectionMode() {
Context context = getContext();
if (context instanceof MainActivity) {
MainActivity activity = (MainActivity) context;
activity.startSupportActionMode(this);
}
}
public void onItemSelectionToggled() {
if (actionMode != null) {
actionMode.setTitle(String.valueOf(contentListAdapter.getSelectedCount()));
actionMode.invalidate();
}
}
public void onExitSelectionMode() {
if (actionMode != null) {
actionMode.finish();
}
}
}

View file

@ -402,6 +402,15 @@ public final class Helper {
}
return followedTags;
}
public static List<Claim> filterDeletedClaims(List<Claim> claims) {
List<Claim> filtered = new ArrayList<>();
for (Claim claim : claims) {
if (!Lbry.abandonedClaimIds.contains(claim.getClaimId())) {
filtered.add(claim);
}
}
return filtered;
}
public static void setWunderbarValue(String value, Context context) {
if (context instanceof MainActivity) {

View file

@ -44,6 +44,7 @@ public final class Lbry {
public static List<Tag> followedTags = new ArrayList<>();
public static List<Claim> ownClaims = new ArrayList<>();
public static List<Claim> ownChannels = new ArrayList<>(); // Make this a subset of ownClaims?
public static List<String> abandonedClaimIds = new ArrayList<>();
public static final int TTL_CLAIM_SEARCH_VALUE = 120000; // 2-minute TTL for cache
public static final String SDK_CONNECTION_STRING = "http://127.0.0.1:5279";
@ -97,6 +98,7 @@ public final class Lbry {
public static boolean SDK_READY = false;
public static void startupInit() {
abandonedClaimIds = new ArrayList<>();
ownChannels = new ArrayList<>();
ownClaims = new ArrayList<>();
knownTags = new ArrayList<>();
@ -474,4 +476,14 @@ public final class Lbry {
claimCache.put(shortUrlKey, claim);
}
}
public static void unsetFilesForCachedClaims(List<String> claimIds) {
for (String claimId : claimIds) {
ClaimCacheKey key = new ClaimCacheKey();
key.setClaimId(claimId);
if (claimCache.containsKey(key)) {
claimCache.get(key).setFile(null);
}
}
}
}

View file

@ -192,8 +192,9 @@ public final class Lbryio {
Gson gson = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create();
User user = gson.fromJson(object.toString(), type);
return user;
} catch (LbryioRequestException | LbryioResponseException | ClassCastException ex) {
android.util.Log.e(TAG, "Cannot retrieve the current user", ex);
} catch (LbryioRequestException | LbryioResponseException | ClassCastException | IllegalStateException ex) {
LbryAnalytics.logException(String.format("/user/me failed: %s", ex.getMessage()), ex.getClass().getName());
android.util.Log.e(TAG, "Could not retrieve the current user", ex);
return null;
}
}

View file

@ -54,6 +54,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="12dp"
android:layout_marginRight="12dp"
android:layout_above="@+id/channel_view_tabs"
android:layout_toRightOf="@id/channel_view_icon_container"
android:orientation="vertical">
@ -66,7 +67,9 @@
android:paddingRight="4dp"
android:paddingTop="2dp"
android:paddingBottom="2dp"
android:ellipsize="end"
android:fontFamily="@font/inter"
android:maxLines="2"
android:textSize="20sp"
android:textColor="@color/white" />
<TextView

View file

@ -300,9 +300,7 @@
android:clipToPadding="false"
android:layout_below="@id/library_storage_stats_card"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingTop="12dp"
android:paddingBottom="16dp" />
android:layout_height="match_parent" />
<RelativeLayout
android:id="@+id/library_empty_container"

View file

@ -9,7 +9,8 @@
android:paddingBottom="8dp"
android:clickable="true"
android:focusable="true"
android:background="?attr/selectableItemBackground">
android:foreground="?attr/selectableItemBackground"
android:background="@drawable/bg_selected_list_item">
<LinearLayout
android:id="@+id/claim_repost_info"
android:layout_width="match_parent"

View file

@ -323,6 +323,7 @@
<string name="item_pending_blockchain">The claim is pending publish on the blockchain. You will be able to access or edit the claim in a few moments.</string>
<string name="pending">Pending</string>
<string name="create">Create</string>
<string name="one_or_more_channels_failed_abandon">One or moe channels could not be deleted at this time. Please try again later.</string>
<plurals name="min_deposit_required">
<item quantity="one">A minimum deposit of %1$s credit is required.</item>
<item quantity="other">A minimum deposit of %1$s credits is required.</item>
@ -331,6 +332,10 @@
<item quantity="one">Are you sure you want to delete the selected channel?</item>
<item quantity="other">Are you sure you want to delete the selected channels?</item>
</plurals>
<plurals name="channels_deleted">
<item quantity="one">The channel was successfully deleted.</item>
<item quantity="other">The channels were successfully deleted.</item>
</plurals>
<!-- Rewards -->
<string name="lbry_credits_allow">LBRY credits allow you to publish or purchase content.</string>
@ -388,6 +393,14 @@
<string name="kb">KB</string>
<string name="gb">GB</string>
<string name="zero_mb">0MB</string>
<plurals name="confirm_delete_files">
<item quantity="one">Are you sure you want to remove the selected file from your device?</item>
<item quantity="other">Are you sure you want to remove the selected files from your device?</item>
</plurals>
<plurals name="files_deleted">
<item quantity="one">The file was successfully deleted.</item>
<item quantity="other">The files were successfully deleted.</item>
</plurals>
<!-- About -->
<string name="about_lbry">About LBRY</string>