From cc2feab37ea674c97731414a72a70e3edb848d9f Mon Sep 17 00:00:00 2001 From: Mauricio Colli Date: Thu, 8 Mar 2018 11:50:46 -0300 Subject: [PATCH] Implement UI for subscriptions import/export - Nice and easy to use import/export options in the subscriptions fragment - Includes instructions for each service (in the import fragment/screen) --- .../newpipe/fragments/BaseStateFragment.java | 7 - .../newpipe/fragments/MainFragment.java | 2 +- .../fragments/list/BaseListFragment.java | 16 +- .../ImportConfirmationDialog.java | 64 +++++ .../subscription/SubscriptionFragment.java | 243 ++++++++++++++++-- .../SubscriptionsImportFragment.java | 210 +++++++++++++++ .../schabi/newpipe/util/AnimationUtils.java | 75 +++++- .../util/FilePickerActivityHelper.java | 116 +++++++++ .../schabi/newpipe/util/NavigationHelper.java | 50 ++-- .../schabi/newpipe/util/ServiceHelper.java | 34 +++ .../org/schabi/newpipe/util/ThemeHelper.java | 95 ++++++- .../schabi/newpipe/views/CollapsibleView.java | 230 +++++++++++++++++ .../drawable-hdpi/ic_backup_black_24dp.png | Bin 0 -> 337 bytes .../drawable-hdpi/ic_backup_white_24dp.png | Bin 0 -> 345 bytes .../ic_import_export_black_24dp.png | Bin 0 -> 163 bytes .../ic_import_export_white_24dp.png | Bin 0 -> 172 bytes .../res/drawable-hdpi/ic_save_black_24dp.png | Bin 0 -> 240 bytes .../res/drawable-hdpi/ic_save_white_24dp.png | Bin 0 -> 247 bytes .../drawable-mdpi/ic_backup_black_24dp.png | Bin 0 -> 226 bytes .../drawable-mdpi/ic_backup_white_24dp.png | Bin 0 -> 235 bytes .../ic_import_export_black_24dp.png | Bin 0 -> 128 bytes .../ic_import_export_white_24dp.png | Bin 0 -> 132 bytes .../res/drawable-mdpi/ic_save_black_24dp.png | Bin 0 -> 167 bytes .../res/drawable-mdpi/ic_save_white_24dp.png | Bin 0 -> 168 bytes .../drawable-xhdpi/ic_backup_black_24dp.png | Bin 0 -> 386 bytes .../drawable-xhdpi/ic_backup_white_24dp.png | Bin 0 -> 405 bytes .../ic_import_export_black_24dp.png | Bin 0 -> 186 bytes .../ic_import_export_white_24dp.png | Bin 0 -> 202 bytes .../res/drawable-xhdpi/ic_save_black_24dp.png | Bin 0 -> 264 bytes .../res/drawable-xhdpi/ic_save_white_24dp.png | Bin 0 -> 273 bytes .../drawable-xxhdpi/ic_backup_black_24dp.png | Bin 0 -> 561 bytes .../drawable-xxhdpi/ic_backup_white_24dp.png | Bin 0 -> 589 bytes .../ic_import_export_black_24dp.png | Bin 0 -> 230 bytes .../ic_import_export_white_24dp.png | Bin 0 -> 252 bytes .../drawable-xxhdpi/ic_save_black_24dp.png | Bin 0 -> 368 bytes .../drawable-xxhdpi/ic_save_white_24dp.png | Bin 0 -> 391 bytes .../drawable-xxxhdpi/ic_backup_black_24dp.png | Bin 0 -> 733 bytes .../drawable-xxxhdpi/ic_backup_white_24dp.png | Bin 0 -> 770 bytes .../ic_import_export_black_24dp.png | Bin 0 -> 302 bytes .../ic_import_export_white_24dp.png | Bin 0 -> 328 bytes .../drawable-xxxhdpi/ic_save_black_24dp.png | Bin 0 -> 477 bytes .../drawable-xxxhdpi/ic_save_white_24dp.png | Bin 0 -> 504 bytes app/src/main/res/layout/fragment_import.xml | 55 ++++ .../main/res/layout/fragment_subscription.xml | 8 +- .../main/res/layout/subscription_header.xml | 154 ++++++++--- .../subscription_import_export_item.xml | 33 +++ app/src/main/res/values-land/dimens.xml | 2 + app/src/main/res/values/attrs.xml | 3 + app/src/main/res/values/colors.xml | 2 + app/src/main/res/values/dimens.xml | 8 + app/src/main/res/values/styles.xml | 23 ++ app/src/main/res/values/styles_misc.xml | 26 +- 52 files changed, 1329 insertions(+), 127 deletions(-) create mode 100644 app/src/main/java/org/schabi/newpipe/fragments/subscription/ImportConfirmationDialog.java create mode 100644 app/src/main/java/org/schabi/newpipe/fragments/subscription/SubscriptionsImportFragment.java create mode 100644 app/src/main/java/org/schabi/newpipe/views/CollapsibleView.java create mode 100644 app/src/main/res/drawable-hdpi/ic_backup_black_24dp.png create mode 100644 app/src/main/res/drawable-hdpi/ic_backup_white_24dp.png create mode 100644 app/src/main/res/drawable-hdpi/ic_import_export_black_24dp.png create mode 100644 app/src/main/res/drawable-hdpi/ic_import_export_white_24dp.png create mode 100644 app/src/main/res/drawable-hdpi/ic_save_black_24dp.png create mode 100644 app/src/main/res/drawable-hdpi/ic_save_white_24dp.png create mode 100644 app/src/main/res/drawable-mdpi/ic_backup_black_24dp.png create mode 100644 app/src/main/res/drawable-mdpi/ic_backup_white_24dp.png create mode 100644 app/src/main/res/drawable-mdpi/ic_import_export_black_24dp.png create mode 100644 app/src/main/res/drawable-mdpi/ic_import_export_white_24dp.png create mode 100644 app/src/main/res/drawable-mdpi/ic_save_black_24dp.png create mode 100644 app/src/main/res/drawable-mdpi/ic_save_white_24dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_backup_black_24dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_backup_white_24dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_import_export_black_24dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_import_export_white_24dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_save_black_24dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_save_white_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_backup_black_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_backup_white_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_import_export_black_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_import_export_white_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_save_black_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_save_white_24dp.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_backup_black_24dp.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_backup_white_24dp.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_import_export_black_24dp.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_import_export_white_24dp.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_save_black_24dp.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_save_white_24dp.png create mode 100644 app/src/main/res/layout/fragment_import.xml create mode 100644 app/src/main/res/layout/subscription_import_export_item.xml diff --git a/app/src/main/java/org/schabi/newpipe/fragments/BaseStateFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/BaseStateFragment.java index a75c8561f..cb9ce8947 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/BaseStateFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/BaseStateFragment.java @@ -246,13 +246,6 @@ public abstract class BaseStateFragment extends BaseFragment implements ViewC // Utils //////////////////////////////////////////////////////////////////////////*/ - public void setTitle(String title) { - if (DEBUG) Log.d(TAG, "setTitle() called with: title = [" + title + "]"); - if (activity != null && activity.getSupportActionBar() != null) { - activity.getSupportActionBar().setTitle(title); - } - } - protected void openUrlInBrowser(String url) { Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); startActivity(Intent.createChooser(intent, activity.getString(R.string.share_dialog_title))); diff --git a/app/src/main/java/org/schabi/newpipe/fragments/MainFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/MainFragment.java index abc150e7d..20607b3a0 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/MainFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/MainFragment.java @@ -106,7 +106,7 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte super.onCreateOptionsMenu(menu, inflater); if (DEBUG) Log.d(TAG, "onCreateOptionsMenu() called with: menu = [" + menu + "], inflater = [" + inflater + "]"); inflater.inflate(R.menu.main_fragment_menu, menu); - SubMenu kioskMenu = menu.addSubMenu(getString(R.string.kiosk)); + SubMenu kioskMenu = menu.addSubMenu(Menu.NONE, Menu.NONE, 200, getString(R.string.kiosk)); try { createKioskMenu(kioskMenu, inflater); } catch (Exception e) { diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java index 8c9945149..580e16825 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java @@ -141,8 +141,7 @@ public abstract class BaseListFragment extends BaseStateFragment implem @Override public void selected(StreamInfoItem selectedItem) { onItemSelected(selectedItem); - NavigationHelper.openVideoDetailFragment( - useAsFrontPage?getParentFragment().getFragmentManager():getFragmentManager(), + NavigationHelper.openVideoDetailFragment(useAsFrontPage ? getParentFragment().getFragmentManager() : getFragmentManager(), selectedItem.getServiceId(), selectedItem.getUrl(), selectedItem.getName()); } @@ -156,8 +155,7 @@ public abstract class BaseListFragment extends BaseStateFragment implem @Override public void selected(ChannelInfoItem selectedItem) { onItemSelected(selectedItem); - NavigationHelper.openChannelFragment( - useAsFrontPage?getParentFragment().getFragmentManager():getFragmentManager(), + NavigationHelper.openChannelFragment(useAsFrontPage ? getParentFragment().getFragmentManager() : getFragmentManager(), selectedItem.getServiceId(), selectedItem.getUrl(), selectedItem.getName()); } }); @@ -166,8 +164,7 @@ public abstract class BaseListFragment extends BaseStateFragment implem @Override public void selected(PlaylistInfoItem selectedItem) { onItemSelected(selectedItem); - NavigationHelper.openPlaylistFragment( - useAsFrontPage?getParentFragment().getFragmentManager():getFragmentManager(), + NavigationHelper.openPlaylistFragment(useAsFrontPage ? getParentFragment().getFragmentManager() : getFragmentManager(), selectedItem.getServiceId(), selectedItem.getUrl(), selectedItem.getName()); } }); @@ -230,7 +227,7 @@ public abstract class BaseListFragment extends BaseStateFragment implem ActionBar supportActionBar = activity.getSupportActionBar(); if (supportActionBar != null) { supportActionBar.setDisplayShowTitleEnabled(true); - if(useAsFrontPage) { + if (useAsFrontPage) { supportActionBar.setDisplayHomeAsUpEnabled(false); } else { supportActionBar.setDisplayHomeAsUpEnabled(true); @@ -277,9 +274,8 @@ public abstract class BaseListFragment extends BaseStateFragment implem @Override public void showListFooter(final boolean show) { - itemsList.post(new Runnable() { - @Override - public void run() { + itemsList.post(() -> { + if (infoListAdapter != null && itemsList != null) { infoListAdapter.showFooter(show); } }); diff --git a/app/src/main/java/org/schabi/newpipe/fragments/subscription/ImportConfirmationDialog.java b/app/src/main/java/org/schabi/newpipe/fragments/subscription/ImportConfirmationDialog.java new file mode 100644 index 000000000..3a64c22c7 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/subscription/ImportConfirmationDialog.java @@ -0,0 +1,64 @@ +package org.schabi.newpipe.fragments.subscription; + +import android.app.AlertDialog; +import android.app.Dialog; +import android.content.Intent; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v4.app.DialogFragment; +import android.support.v4.app.Fragment; + +import org.schabi.newpipe.R; +import org.schabi.newpipe.util.ThemeHelper; + +import icepick.Icepick; +import icepick.State; + +public class ImportConfirmationDialog extends DialogFragment { + @State + protected Intent resultServiceIntent; + + public void setResultServiceIntent(Intent resultServiceIntent) { + this.resultServiceIntent = resultServiceIntent; + } + + public static void show(@NonNull Fragment fragment, @NonNull Intent resultServiceIntent) { + if (fragment.getFragmentManager() == null) return; + + final ImportConfirmationDialog confirmationDialog = new ImportConfirmationDialog(); + confirmationDialog.setResultServiceIntent(resultServiceIntent); + confirmationDialog.show(fragment.getFragmentManager(), null); + } + + @NonNull + @Override + public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { + return new AlertDialog.Builder(getContext(), ThemeHelper.getDialogTheme(getContext())) + .setMessage(R.string.import_network_expensive_warning) + .setCancelable(true) + .setNegativeButton(R.string.cancel, null) + .setPositiveButton(android.R.string.ok, (dialogInterface, i) -> { + if (resultServiceIntent != null && getContext() != null) { + getContext().startService(resultServiceIntent); + } + dismiss(); + }) + .create(); + } + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + if (resultServiceIntent == null) throw new IllegalStateException("Result intent is null"); + + Icepick.restoreInstanceState(this, savedInstanceState); + } + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + Icepick.saveInstanceState(this, outState); + } +} diff --git a/app/src/main/java/org/schabi/newpipe/fragments/subscription/SubscriptionFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/subscription/SubscriptionFragment.java index a91cca908..1e69732b7 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/subscription/SubscriptionFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/subscription/SubscriptionFragment.java @@ -1,30 +1,62 @@ package org.schabi.newpipe.fragments.subscription; +import android.app.Activity; +import android.content.BroadcastReceiver; import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.graphics.Color; +import android.graphics.PorterDuff; import android.os.Bundle; +import android.os.Environment; import android.os.Parcelable; +import android.support.annotation.DrawableRes; import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import android.support.v4.content.LocalBroadcastManager; +import android.support.v7.app.ActionBar; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; import android.view.View; import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; +import android.widget.Toast; + +import com.nononsenseapps.filepicker.Utils; import org.schabi.newpipe.R; import org.schabi.newpipe.database.subscription.SubscriptionEntity; import org.schabi.newpipe.extractor.InfoItem; +import org.schabi.newpipe.extractor.NewPipe; +import org.schabi.newpipe.extractor.StreamingService; import org.schabi.newpipe.extractor.channel.ChannelInfoItem; +import org.schabi.newpipe.extractor.exceptions.ExtractionException; +import org.schabi.newpipe.extractor.subscription.SubscriptionExtractor; import org.schabi.newpipe.fragments.BaseStateFragment; import org.schabi.newpipe.info_list.InfoListAdapter; import org.schabi.newpipe.report.UserAction; +import org.schabi.newpipe.subscription.SubscriptionService; +import org.schabi.newpipe.subscription.services.SubscriptionsExportService; +import org.schabi.newpipe.subscription.services.SubscriptionsImportService; +import org.schabi.newpipe.util.FilePickerActivityHelper; import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.OnClickGesture; +import org.schabi.newpipe.util.ServiceHelper; +import org.schabi.newpipe.util.ThemeHelper; +import org.schabi.newpipe.views.CollapsibleView; +import java.io.File; +import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; +import java.util.Date; import java.util.List; +import java.util.Locale; import icepick.State; import io.reactivex.Observer; @@ -33,18 +65,29 @@ import io.reactivex.disposables.CompositeDisposable; import io.reactivex.disposables.Disposable; import io.reactivex.schedulers.Schedulers; +import static org.schabi.newpipe.subscription.services.SubscriptionsImportService.KEY_MODE; +import static org.schabi.newpipe.subscription.services.SubscriptionsImportService.KEY_VALUE; +import static org.schabi.newpipe.subscription.services.SubscriptionsImportService.PREVIOUS_EXPORT_MODE; +import static org.schabi.newpipe.util.AnimationUtils.animateRotation; import static org.schabi.newpipe.util.AnimationUtils.animateView; public class SubscriptionFragment extends BaseStateFragment> { - private View headerRootLayout; + private static final int REQUEST_EXPORT_CODE = 666; + private static final int REQUEST_IMPORT_CODE = 667; - private InfoListAdapter infoListAdapter; private RecyclerView itemsList; - @State protected Parcelable itemsListState; + private InfoListAdapter infoListAdapter; + + private View headerRootLayout; + private View whatsNewItemListHeader; + private View importExportListHeader; + + @State + protected Parcelable importExportOptionsState; + private CollapsibleView importExportOptions; - /* Used for independent events */ private CompositeDisposable disposables = new CompositeDisposable(); private SubscriptionService subscriptionService; @@ -52,39 +95,48 @@ public class SubscriptionFragment extends BaseStateFragment onImportPreviousSelected()); + + final int iconColor = ThemeHelper.isLightThemeSelected(getContext()) ? Color.BLACK : Color.WHITE; + final String[] services = getResources().getStringArray(R.array.service_list); + for (String serviceName : services) { + try { + final StreamingService service = NewPipe.getService(serviceName); + + final SubscriptionExtractor subscriptionExtractor = service.getSubscriptionExtractor(); + if (subscriptionExtractor == null) continue; + + final List supportedSources = subscriptionExtractor.getSupportedSources(); + if (supportedSources.isEmpty()) continue; + + final View itemView = addItemView(serviceName, ServiceHelper.getIcon(service.getServiceId()), listHolder); + final ImageView iconView = itemView.findViewById(android.R.id.icon1); + iconView.setColorFilter(iconColor, PorterDuff.Mode.SRC_IN); + + itemView.setOnClickListener(selectedItem -> onImportFromServiceSelected(service.getServiceId())); + } catch (ExtractionException e) { + throw new RuntimeException("Services array contains an entry that it's not a valid service name (" + serviceName + ")", e); + } + } + } + + private void setupExportToItems(final ViewGroup listHolder) { + final View previousBackupItem = addItemView(getString(R.string.file), ThemeHelper.resolveResourceIdFromAttr(getContext(), R.attr.ic_save), listHolder); + previousBackupItem.setOnClickListener(item -> onExportSelected()); + } + + private void onImportFromServiceSelected(int serviceId) { + if (getParentFragment() == null) return; + NavigationHelper.openSubscriptionsImportFragment(getParentFragment().getFragmentManager(), serviceId); + } + + private void onImportPreviousSelected() { + startActivityForResult(FilePickerActivityHelper.chooseSingleFile(activity), REQUEST_IMPORT_CODE); + } + + private void onExportSelected() { + final String date = new SimpleDateFormat("yyyyMMddHHmm", Locale.ENGLISH).format(new Date()); + final String exportName = "newpipe_subscriptions_" + date + ".json"; + final File exportFile = new File(Environment.getExternalStorageDirectory(), exportName); + + startActivityForResult(FilePickerActivityHelper.chooseFileToSave(activity, exportFile.getAbsolutePath()), REQUEST_EXPORT_CODE); + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + if (data != null && data.getData() != null && resultCode == Activity.RESULT_OK) { + if (requestCode == REQUEST_EXPORT_CODE) { + final File exportFile = Utils.getFileForUri(data.getData()); + if (!exportFile.getParentFile().canWrite() || !exportFile.getParentFile().canRead()) { + Toast.makeText(activity, R.string.invalid_directory, Toast.LENGTH_SHORT).show(); + } else { + activity.startService(new Intent(activity, SubscriptionsExportService.class) + .putExtra(SubscriptionsExportService.KEY_FILE_PATH, exportFile.getAbsolutePath())); + } + } else if (requestCode == REQUEST_IMPORT_CODE) { + final String path = Utils.getFileForUri(data.getData()).getAbsolutePath(); + ImportConfirmationDialog.show(this, new Intent(activity, SubscriptionsImportService.class) + .putExtra(KEY_MODE, PREVIOUS_EXPORT_MODE) + .putExtra(KEY_VALUE, path)); + } + } + } + /*///////////////////////////////////////////////////////////////////////// // Fragment Views - /////////////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////////*/ @Override protected void initViews(View rootView, Bundle savedInstanceState) { @@ -116,9 +290,27 @@ public class SubscriptionFragment extends BaseStateFragment animateRotation(iconView, 250, newState == CollapsibleView.COLLAPSED ? 0 : 180); } @Override @@ -130,12 +322,14 @@ public class SubscriptionFragment extends BaseStateFragment + //noinspection ConstantConditions + whatsNewItemListHeader.setOnClickListener(v -> NavigationHelper.openWhatsNewFragment(getParentFragment().getFragmentManager())); + importExportListHeader.setOnClickListener(v -> importExportOptions.switchState()); } private void resetFragment() { @@ -189,6 +383,7 @@ public class SubscriptionFragment extends BaseStateFragment supportedSources; + private String relatedUrl; + @StringRes + private int instructionsString; + + public static SubscriptionsImportFragment getInstance(int serviceId) { + SubscriptionsImportFragment instance = new SubscriptionsImportFragment(); + instance.setInitialData(serviceId); + return instance; + } + + public void setInitialData(int serviceId) { + this.currentServiceId = serviceId; + } + + /*////////////////////////////////////////////////////////////////////////// + // Views + //////////////////////////////////////////////////////////////////////////*/ + + private TextView infoTextView; + + private EditText inputText; + private Button inputButton; + + /////////////////////////////////////////////////////////////////////////// + // Fragment LifeCycle + /////////////////////////////////////////////////////////////////////////// + + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setupServiceVariables(); + if (supportedSources.isEmpty() && currentServiceId != Constants.NO_SERVICE_ID) { + ErrorActivity.reportError(activity, Collections.emptyList(), null, null, ErrorActivity.ErrorInfo.make(UserAction.SOMETHING_ELSE, + NewPipe.getNameOfService(currentServiceId), "Service don't support importing", R.string.general_error)); + activity.finish(); + } + } + + @Override + public void setUserVisibleHint(boolean isVisibleToUser) { + super.setUserVisibleHint(isVisibleToUser); + if (isVisibleToUser) { + setTitle(getString(R.string.import_title)); + } + } + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) { + return inflater.inflate(R.layout.fragment_import, container, false); + } + + /*///////////////////////////////////////////////////////////////////////// + // Fragment Views + /////////////////////////////////////////////////////////////////////////*/ + + @Override + protected void initViews(View rootView, Bundle savedInstanceState) { + super.initViews(rootView, savedInstanceState); + + inputButton = rootView.findViewById(R.id.input_button); + inputText = rootView.findViewById(R.id.input_text); + + infoTextView = rootView.findViewById(R.id.info_text_view); + + // TODO: Support services that can import from more than one source (show the option to the user) + if (supportedSources.contains(CHANNEL_URL)) { + inputButton.setText(R.string.import_title); + inputText.setVisibility(View.VISIBLE); + inputText.setHint(ServiceHelper.getImportInstructionsHint(currentServiceId)); + } else { + inputButton.setText(R.string.import_file_title); + } + + if (instructionsString != 0) { + if (TextUtils.isEmpty(relatedUrl)) { + setInfoText(getString(instructionsString)); + } else { + setInfoText(getString(instructionsString, relatedUrl)); + } + } else { + setInfoText(""); + } + + ActionBar supportActionBar = activity.getSupportActionBar(); + if (supportActionBar != null) { + supportActionBar.setDisplayShowTitleEnabled(true); + setTitle(getString(R.string.import_title)); + } + } + + @Override + protected void initListeners() { + super.initListeners(); + inputButton.setOnClickListener(v -> onImportClicked()); + } + + private void onImportClicked() { + if (inputText.getVisibility() == View.VISIBLE) { + final String value = inputText.getText().toString(); + if (!value.isEmpty()) onImportUrl(value); + } else { + onImportFile(); + } + } + + public void onImportUrl(String value) { + ImportConfirmationDialog.show(this, new Intent(activity, SubscriptionsImportService.class) + .putExtra(KEY_MODE, CHANNEL_URL_MODE) + .putExtra(KEY_VALUE, value) + .putExtra(Constants.KEY_SERVICE_ID, currentServiceId)); + } + + public void onImportFile() { + startActivityForResult(FilePickerActivityHelper.chooseSingleFile(activity), REQUEST_IMPORT_FILE_CODE); + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + if (data == null) return; + + if (resultCode == Activity.RESULT_OK && requestCode == REQUEST_IMPORT_FILE_CODE && data.getData() != null) { + final String path = Utils.getFileForUri(data.getData()).getAbsolutePath(); + ImportConfirmationDialog.show(this, new Intent(activity, SubscriptionsImportService.class) + .putExtra(KEY_MODE, INPUT_STREAM_MODE) + .putExtra(KEY_VALUE, path) + .putExtra(Constants.KEY_SERVICE_ID, currentServiceId)); + } + } + + /////////////////////////////////////////////////////////////////////////// + // Subscriptions + /////////////////////////////////////////////////////////////////////////// + + private void setupServiceVariables() { + if (currentServiceId != Constants.NO_SERVICE_ID) { + try { + final SubscriptionExtractor extractor = NewPipe.getService(currentServiceId).getSubscriptionExtractor(); + supportedSources = extractor.getSupportedSources(); + relatedUrl = extractor.getRelatedUrl(); + instructionsString = ServiceHelper.getImportInstructions(currentServiceId); + return; + } catch (ExtractionException ignored) { + } + } + + supportedSources = Collections.emptyList(); + relatedUrl = null; + instructionsString = 0; + } + + private void setInfoText(String infoString) { + infoTextView.setText(infoString); + LinkifyCompat.addLinks(infoTextView, Linkify.WEB_URLS); + } +} diff --git a/app/src/main/java/org/schabi/newpipe/util/AnimationUtils.java b/app/src/main/java/org/schabi/newpipe/util/AnimationUtils.java index c954211fa..3c5f16929 100644 --- a/app/src/main/java/org/schabi/newpipe/util/AnimationUtils.java +++ b/app/src/main/java/org/schabi/newpipe/util/AnimationUtils.java @@ -1,3 +1,22 @@ +/* + * Copyright 2018 Mauricio Colli + * AnimationUtils.java is part of NewPipe + * + * License: GPL-3.0+ + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + package org.schabi.newpipe.util; import android.animation.Animator; @@ -19,7 +38,9 @@ public class AnimationUtils { private static final boolean DEBUG = MainActivity.DEBUG; public enum Type { - ALPHA, SCALE_AND_ALPHA, LIGHT_SCALE_AND_ALPHA, SLIDE_AND_ALPHA, LIGHT_SLIDE_AND_ALPHA + ALPHA, + SCALE_AND_ALPHA, LIGHT_SCALE_AND_ALPHA, + SLIDE_AND_ALPHA, LIGHT_SLIDE_AND_ALPHA } public static void animateView(View view, boolean enterOrExit, long duration) { @@ -168,6 +189,58 @@ public class AnimationUtils { viewPropertyAnimator.start(); } + public static ValueAnimator animateHeight(final View view, long duration, int targetHeight) { + final int height = view.getHeight(); + if (DEBUG) { + Log.d(TAG, "animateHeight: duration = [" + duration + "], from " + height + " to → " + targetHeight + " in: " + view); + } + + ValueAnimator animator = ValueAnimator.ofFloat(height, targetHeight); + animator.setInterpolator(new FastOutSlowInInterpolator()); + animator.setDuration(duration); + animator.addUpdateListener(animation -> { + final float value = (float) animation.getAnimatedValue(); + view.getLayoutParams().height = (int) value; + view.requestLayout(); + }); + animator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + view.getLayoutParams().height = targetHeight; + view.requestLayout(); + } + + @Override + public void onAnimationCancel(Animator animation) { + view.getLayoutParams().height = targetHeight; + view.requestLayout(); + } + }); + animator.start(); + + return animator; + } + + public static void animateRotation(final View view, long duration, int targetRotation) { + if (DEBUG) { + Log.d(TAG, "animateRotation: duration = [" + duration + "], from " + view.getRotation() + " to → " + targetRotation + " in: " + view); + } + view.animate().setListener(null).cancel(); + + view.animate().rotation(targetRotation).setDuration(duration).setInterpolator(new FastOutSlowInInterpolator()) + .setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationCancel(Animator animation) { + view.setRotation(targetRotation); + } + + @Override + public void onAnimationEnd(Animator animation) { + view.setRotation(targetRotation); + } + }).start(); + } + /*////////////////////////////////////////////////////////////////////////// // Internals //////////////////////////////////////////////////////////////////////////*/ diff --git a/app/src/main/java/org/schabi/newpipe/util/FilePickerActivityHelper.java b/app/src/main/java/org/schabi/newpipe/util/FilePickerActivityHelper.java index 5f588c5ca..20554ce59 100644 --- a/app/src/main/java/org/schabi/newpipe/util/FilePickerActivityHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/FilePickerActivityHelper.java @@ -1,10 +1,32 @@ package org.schabi.newpipe.util; +import android.content.Context; +import android.content.Intent; import android.os.Bundle; +import android.os.Environment; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v4.content.Loader; +import android.support.v7.util.SortedList; +import android.support.v7.widget.RecyclerView; +import android.util.TypedValue; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; +import android.widget.Toast; + +import com.nononsenseapps.filepicker.AbstractFilePickerFragment; +import com.nononsenseapps.filepicker.FilePickerFragment; + import org.schabi.newpipe.R; +import java.io.File; + public class FilePickerActivityHelper extends com.nononsenseapps.filepicker.FilePickerActivity { + private CustomFilePickerFragment currentFragment; + @Override public void onCreate(Bundle savedInstanceState) { if(ThemeHelper.isLightThemeSelected(this)) { @@ -14,4 +36,98 @@ public class FilePickerActivityHelper extends com.nononsenseapps.filepicker.File } super.onCreate(savedInstanceState); } + + @Override + public void onBackPressed() { + // If at top most level, normal behaviour + if (currentFragment.isBackTop()) { + super.onBackPressed(); + } else { + // Else go up + currentFragment.goUp(); + } + } + + @Override + protected AbstractFilePickerFragment getFragment(@Nullable String startPath, int mode, boolean allowMultiple, boolean allowCreateDir, boolean allowExistingFile, boolean singleClick) { + final CustomFilePickerFragment fragment = new CustomFilePickerFragment(); + fragment.setArgs(startPath != null ? startPath : Environment.getExternalStorageDirectory().getPath(), + mode, allowMultiple, allowCreateDir, allowExistingFile, singleClick); + return currentFragment = fragment; + } + + public static Intent chooseSingleFile(@NonNull Context context) { + return new Intent(context, FilePickerActivityHelper.class) + .putExtra(FilePickerActivityHelper.EXTRA_ALLOW_MULTIPLE, false) + .putExtra(FilePickerActivityHelper.EXTRA_ALLOW_CREATE_DIR, false) + .putExtra(FilePickerActivityHelper.EXTRA_SINGLE_CLICK, true) + .putExtra(FilePickerActivityHelper.EXTRA_MODE, FilePickerActivityHelper.MODE_FILE); + } + + public static Intent chooseFileToSave(@NonNull Context context, @Nullable String startPath) { + return new Intent(context, FilePickerActivityHelper.class) + .putExtra(FilePickerActivityHelper.EXTRA_ALLOW_MULTIPLE, false) + .putExtra(FilePickerActivityHelper.EXTRA_ALLOW_CREATE_DIR, true) + .putExtra(FilePickerActivityHelper.EXTRA_ALLOW_EXISTING_FILE, true) + .putExtra(FilePickerActivityHelper.EXTRA_START_PATH, startPath) + .putExtra(FilePickerActivityHelper.EXTRA_MODE, FilePickerActivityHelper.MODE_NEW_FILE); + } + + /*////////////////////////////////////////////////////////////////////////// + // Internal + //////////////////////////////////////////////////////////////////////////*/ + + public static class CustomFilePickerFragment extends FilePickerFragment { + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + return super.onCreateView(inflater, container, savedInstanceState); + } + + @NonNull + @Override + public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + final RecyclerView.ViewHolder viewHolder = super.onCreateViewHolder(parent, viewType); + + final View view = viewHolder.itemView.findViewById(android.R.id.text1); + if (view instanceof TextView) { + ((TextView) view).setTextSize(TypedValue.COMPLEX_UNIT_PX, getResources().getDimension(R.dimen.file_picker_items_text_size)); + } + + return viewHolder; + } + + @Override + public void onClickOk(@NonNull View view) { + if (mode == MODE_NEW_FILE && getNewFileName().isEmpty()) { + if (mToast != null) mToast.cancel(); + mToast = Toast.makeText(getActivity(), R.string.file_name_empty_error, Toast.LENGTH_SHORT); + mToast.show(); + return; + } + + super.onClickOk(view); + } + + public File getBackTop() { + if (getArguments() == null) return Environment.getExternalStorageDirectory(); + + final String path = getArguments().getString(KEY_START_PATH, "/"); + if (path.contains(Environment.getExternalStorageDirectory().getPath())) { + return Environment.getExternalStorageDirectory(); + } + + return getPath(path); + } + + public boolean isBackTop() { + return compareFiles(mCurrentPath, getBackTop()) == 0 || compareFiles(mCurrentPath, new File("/")) == 0; + } + + @Override + public void onLoadFinished(Loader> loader, SortedList data) { + super.onLoadFinished(loader, data); + layoutManager.scrollToPosition(0); + } + } } diff --git a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java index ee94ac81f..26088a64c 100644 --- a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java @@ -1,5 +1,6 @@ package org.schabi.newpipe.util; +import android.annotation.SuppressLint; import android.app.Activity; import android.content.ActivityNotFoundException; import android.content.Context; @@ -11,6 +12,7 @@ import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentManager; +import android.support.v4.app.FragmentTransaction; import android.support.v7.app.AlertDialog; import android.util.Log; import android.widget.Toast; @@ -38,6 +40,7 @@ import org.schabi.newpipe.fragments.list.search.SearchFragment; import org.schabi.newpipe.fragments.local.bookmark.LastPlayedFragment; import org.schabi.newpipe.fragments.local.bookmark.LocalPlaylistFragment; import org.schabi.newpipe.fragments.local.bookmark.MostPlayedFragment; +import org.schabi.newpipe.fragments.subscription.SubscriptionsImportFragment; import org.schabi.newpipe.history.HistoryActivity; import org.schabi.newpipe.player.BackgroundPlayer; import org.schabi.newpipe.player.BackgroundPlayerActivity; @@ -247,6 +250,12 @@ public class NavigationHelper { // Through FragmentManager //////////////////////////////////////////////////////////////////////////*/ + @SuppressLint("CommitTransaction") + private static FragmentTransaction defaultTransaction(FragmentManager fragmentManager) { + return fragmentManager.beginTransaction() + .setCustomAnimations(R.animator.custom_fade_in, R.animator.custom_fade_out, R.animator.custom_fade_in, R.animator.custom_fade_out); + } + public static void gotoMainFragment(FragmentManager fragmentManager) { ImageLoader.getInstance().clearMemoryCache(); @@ -258,8 +267,7 @@ public class NavigationHelper { InfoCache.getInstance().trimCache(); fragmentManager.popBackStackImmediate(null, FragmentManager.POP_BACK_STACK_INCLUSIVE); - fragmentManager.beginTransaction() - .setCustomAnimations(R.animator.custom_fade_in, R.animator.custom_fade_out, R.animator.custom_fade_in, R.animator.custom_fade_out) + defaultTransaction(fragmentManager) .replace(R.id.fragment_holder, new MainFragment()) .addToBackStack(MAIN_FRAGMENT_TAG) .commit(); @@ -276,8 +284,7 @@ public class NavigationHelper { } public static void openSearchFragment(FragmentManager fragmentManager, int serviceId, String query) { - fragmentManager.beginTransaction() - .setCustomAnimations(R.animator.custom_fade_in, R.animator.custom_fade_out, R.animator.custom_fade_in, R.animator.custom_fade_out) + defaultTransaction(fragmentManager) .replace(R.id.fragment_holder, SearchFragment.getInstance(serviceId, query)) .addToBackStack(SEARCH_FRAGMENT_TAG) .commit(); @@ -301,8 +308,7 @@ public class NavigationHelper { VideoDetailFragment instance = VideoDetailFragment.getInstance(serviceId, url, title); instance.setAutoplay(autoPlay); - fragmentManager.beginTransaction() - .setCustomAnimations(R.animator.custom_fade_in, R.animator.custom_fade_out, R.animator.custom_fade_in, R.animator.custom_fade_out) + defaultTransaction(fragmentManager) .replace(R.id.fragment_holder, instance) .addToBackStack(null) .commit(); @@ -310,8 +316,7 @@ public class NavigationHelper { public static void openChannelFragment(FragmentManager fragmentManager, int serviceId, String url, String name) { if (name == null) name = ""; - fragmentManager.beginTransaction() - .setCustomAnimations(R.animator.custom_fade_in, R.animator.custom_fade_out, R.animator.custom_fade_in, R.animator.custom_fade_out) + defaultTransaction(fragmentManager) .replace(R.id.fragment_holder, ChannelFragment.getInstance(serviceId, url, name)) .addToBackStack(null) .commit(); @@ -319,25 +324,21 @@ public class NavigationHelper { public static void openPlaylistFragment(FragmentManager fragmentManager, int serviceId, String url, String name) { if (name == null) name = ""; - fragmentManager.beginTransaction() - .setCustomAnimations(R.animator.custom_fade_in, R.animator.custom_fade_out, R.animator.custom_fade_in, R.animator.custom_fade_out) + defaultTransaction(fragmentManager) .replace(R.id.fragment_holder, PlaylistFragment.getInstance(serviceId, url, name)) .addToBackStack(null) .commit(); } public static void openWhatsNewFragment(FragmentManager fragmentManager) { - fragmentManager.beginTransaction() - .setCustomAnimations(R.animator.custom_fade_in, R.animator.custom_fade_out, R.animator.custom_fade_in, R.animator.custom_fade_out) + defaultTransaction(fragmentManager) .replace(R.id.fragment_holder, new FeedFragment()) .addToBackStack(null) .commit(); } - public static void openKioskFragment(FragmentManager fragmentManager, int serviceId, String kioskId) - throws ExtractionException { - fragmentManager.beginTransaction() - .setCustomAnimations(R.animator.custom_fade_in, R.animator.custom_fade_out, R.animator.custom_fade_in, R.animator.custom_fade_out) + public static void openKioskFragment(FragmentManager fragmentManager, int serviceId, String kioskId) throws ExtractionException { + defaultTransaction(fragmentManager) .replace(R.id.fragment_holder, KioskFragment.getInstance(serviceId, kioskId)) .addToBackStack(null) .commit(); @@ -345,28 +346,33 @@ public class NavigationHelper { public static void openLocalPlaylistFragment(FragmentManager fragmentManager, long playlistId, String name) { if (name == null) name = ""; - fragmentManager.beginTransaction() - .setCustomAnimations(R.animator.custom_fade_in, R.animator.custom_fade_out, R.animator.custom_fade_in, R.animator.custom_fade_out) + defaultTransaction(fragmentManager) .replace(R.id.fragment_holder, LocalPlaylistFragment.getInstance(playlistId, name)) .addToBackStack(null) .commit(); } public static void openLastPlayedFragment(FragmentManager fragmentManager) { - fragmentManager.beginTransaction() - .setCustomAnimations(R.animator.custom_fade_in, R.animator.custom_fade_out, R.animator.custom_fade_in, R.animator.custom_fade_out) + defaultTransaction(fragmentManager) .replace(R.id.fragment_holder, new LastPlayedFragment()) .addToBackStack(null) .commit(); } public static void openMostPlayedFragment(FragmentManager fragmentManager) { - fragmentManager.beginTransaction() - .setCustomAnimations(R.animator.custom_fade_in, R.animator.custom_fade_out, R.animator.custom_fade_in, R.animator.custom_fade_out) + defaultTransaction(fragmentManager) .replace(R.id.fragment_holder, new MostPlayedFragment()) .addToBackStack(null) .commit(); } + + public static void openSubscriptionsImportFragment(FragmentManager fragmentManager, int serviceId) { + defaultTransaction(fragmentManager) + .replace(R.id.fragment_holder, SubscriptionsImportFragment.getInstance(serviceId)) + .addToBackStack(null) + .commit(); + } + /*////////////////////////////////////////////////////////////////////////// // Through Intents //////////////////////////////////////////////////////////////////////////*/ diff --git a/app/src/main/java/org/schabi/newpipe/util/ServiceHelper.java b/app/src/main/java/org/schabi/newpipe/util/ServiceHelper.java index 55c6e68f2..7d71750eb 100644 --- a/app/src/main/java/org/schabi/newpipe/util/ServiceHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/ServiceHelper.java @@ -3,6 +3,7 @@ package org.schabi.newpipe.util; import android.content.Context; import android.preference.PreferenceManager; import android.support.annotation.DrawableRes; +import android.support.annotation.StringRes; import org.schabi.newpipe.BuildConfig; import org.schabi.newpipe.R; @@ -26,6 +27,39 @@ public class ServiceHelper { } } + /** + * Get a resource string with instructions for importing subscriptions for each service. + * + * @return the string resource containing the instructions or -1 if the service don't support it + */ + @StringRes + public static int getImportInstructions(int serviceId) { + switch (serviceId) { + case 0: + return R.string.import_youtube_instructions; + case 1: + return R.string.import_soundcloud_instructions; + default: + return -1; + } + } + + /** + * For services that support importing from a channel url, return a hint that will + * be used in the EditText that the user will type in his channel url. + * + * @return the hint's string resource or -1 if the service don't support it + */ + @StringRes + public static int getImportInstructionsHint(int serviceId) { + switch (serviceId) { + case 1: + return R.string.import_soundcloud_instructions_hint; + default: + return -1; + } + } + public static int getSelectedServiceId(Context context) { if (BuildConfig.BUILD_TYPE.equals("release")) return DEFAULT_FALLBACK_SERVICE.getServiceId(); diff --git a/app/src/main/java/org/schabi/newpipe/util/ThemeHelper.java b/app/src/main/java/org/schabi/newpipe/util/ThemeHelper.java index 824ac4a9d..1edc4dfec 100644 --- a/app/src/main/java/org/schabi/newpipe/util/ThemeHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/ThemeHelper.java @@ -1,3 +1,22 @@ +/* + * Copyright 2018 Mauricio Colli + * ThemeHelper.java is part of NewPipe + * + * License: GPL-3.0+ + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + package org.schabi.newpipe.util; import android.content.Context; @@ -5,6 +24,9 @@ import android.content.res.TypedArray; import android.preference.PreferenceManager; import android.support.annotation.AttrRes; import android.support.annotation.StyleRes; +import android.support.v4.content.ContextCompat; +import android.util.TypedValue; +import android.view.ContextThemeWrapper; import org.schabi.newpipe.R; import org.schabi.newpipe.extractor.NewPipe; @@ -41,16 +63,57 @@ public class ThemeHelper { * @param context context to get the preference */ public static boolean isLightThemeSelected(Context context) { - return getSelectedTheme(context).equals(context.getResources().getString(R.string.light_theme_key)); + return getSelectedThemeString(context).equals(context.getResources().getString(R.string.light_theme_key)); } + + /** + * Create and return a wrapped context with the default selected theme set. + * + * @param baseContext the base context for the wrapper + * @return a wrapped-styled context + */ + public static Context getThemedContext(Context baseContext) { + return new ContextThemeWrapper(baseContext, getThemeForService(baseContext, -1)); + } + + /** + * Return the selected theme without being styled to any service (see {@link #getThemeForService(Context, int)}). + * + * @param context context to get the selected theme + * @return the selected style (the default one) + */ + @StyleRes + public static int getDefaultTheme(Context context) { + return getThemeForService(context, -1); + } + + /** + * Return a dialog theme styled according to the (default) selected theme. + * + * @param context context to get the selected theme + * @return the dialog style (the default one) + */ + @StyleRes + public static int getDialogTheme(Context context) { + return isLightThemeSelected(context) ? R.style.LightDialogTheme : R.style.DarkDialogTheme; + } + + /** + * Return the selected theme styled according to the serviceId. + * + * @param context context to get the selected theme + * @param serviceId return a theme styled to this service, + * -1 to get the default + * @return the selected style (styled) + */ @StyleRes public static int getThemeForService(Context context, int serviceId) { String lightTheme = context.getResources().getString(R.string.light_theme_key); String darkTheme = context.getResources().getString(R.string.dark_theme_key); String blackTheme = context.getResources().getString(R.string.black_theme_key); - String selectedTheme = getSelectedTheme(context); + String selectedTheme = getSelectedThemeString(context); int defaultTheme = R.style.DarkTheme; if (selectedTheme.equals(lightTheme)) defaultTheme = R.style.LightTheme; @@ -83,19 +146,13 @@ public class ThemeHelper { return defaultTheme; } - public static String getSelectedTheme(Context context) { - String themeKey = context.getString(R.string.theme_key); - String defaultTheme = context.getResources().getString(R.string.default_theme_value); - return PreferenceManager.getDefaultSharedPreferences(context).getString(themeKey, defaultTheme); - } - @StyleRes public static int getSettingsThemeStyle(Context context) { String lightTheme = context.getResources().getString(R.string.light_theme_key); String darkTheme = context.getResources().getString(R.string.dark_theme_key); String blackTheme = context.getResources().getString(R.string.black_theme_key); - String selectedTheme = getSelectedTheme(context); + String selectedTheme = getSelectedThemeString(context); if (selectedTheme.equals(lightTheme)) return R.style.LightSettingsTheme; else if (selectedTheme.equals(blackTheme)) return R.style.BlackSettingsTheme; @@ -113,4 +170,24 @@ public class ThemeHelper { a.recycle(); return attributeResourceId; } + + /** + * Get a color from an attr styled according to the the context's theme. + */ + public static int resolveColorFromAttr(Context context, @AttrRes int attrColor) { + final TypedValue value = new TypedValue(); + context.getTheme().resolveAttribute(attrColor, value, true); + + if (value.resourceId != 0) { + return ContextCompat.getColor(context, value.resourceId); + } + + return value.data; + } + + private static String getSelectedThemeString(Context context) { + String themeKey = context.getString(R.string.theme_key); + String defaultTheme = context.getResources().getString(R.string.default_theme_value); + return PreferenceManager.getDefaultSharedPreferences(context).getString(themeKey, defaultTheme); + } } diff --git a/app/src/main/java/org/schabi/newpipe/views/CollapsibleView.java b/app/src/main/java/org/schabi/newpipe/views/CollapsibleView.java new file mode 100644 index 000000000..adef7e76f --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/views/CollapsibleView.java @@ -0,0 +1,230 @@ +/* + * Copyright 2018 Mauricio Colli + * CollapsibleView.java is part of NewPipe + * + * License: GPL-3.0+ + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.schabi.newpipe.views; + +import android.animation.ValueAnimator; +import android.content.Context; +import android.os.Build; +import android.os.Parcelable; +import android.support.annotation.IntDef; +import android.support.annotation.Nullable; +import android.support.annotation.RequiresApi; +import android.util.AttributeSet; +import android.util.Log; +import android.widget.LinearLayout; + +import org.schabi.newpipe.util.AnimationUtils; + +import java.lang.annotation.Retention; +import java.util.ArrayList; +import java.util.List; + +import icepick.Icepick; +import icepick.State; + +import static java.lang.annotation.RetentionPolicy.SOURCE; +import static org.schabi.newpipe.MainActivity.DEBUG; + +/** + * A view that can be fully collapsed and expanded. + */ +public class CollapsibleView extends LinearLayout { + private static final String TAG = CollapsibleView.class.getSimpleName(); + + public CollapsibleView(Context context) { + super(context); + } + + public CollapsibleView(Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + } + + public CollapsibleView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + public CollapsibleView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + /*////////////////////////////////////////////////////////////////////////// + // Collapse/expand logic + //////////////////////////////////////////////////////////////////////////*/ + + private static final int ANIMATION_DURATION = 420; + public static final int COLLAPSED = 0, EXPANDED = 1; + + @Retention(SOURCE) + @IntDef({COLLAPSED, EXPANDED}) + public @interface ViewMode {} + + @State @ViewMode int currentState = COLLAPSED; + private boolean readyToChangeState; + + private int targetHeight = -1; + private ValueAnimator currentAnimator; + private List listeners = new ArrayList<>(); + + /** + * This method recalculates the height of this view so it must be called when + * some child changes (e.g. add new views, change text). + */ + public void ready() { + if (DEBUG) { + Log.d(TAG, getDebugLogString("ready() called")); + } + + measure(MeasureSpec.makeMeasureSpec(getWidth(), MeasureSpec.AT_MOST), MeasureSpec.UNSPECIFIED); + targetHeight = getMeasuredHeight(); + + getLayoutParams().height = currentState == COLLAPSED ? 0 : targetHeight; + requestLayout(); + broadcastState(); + + readyToChangeState = true; + + if (DEBUG) { + Log.d(TAG, getDebugLogString("ready() *after* measuring")); + } + } + + public void collapse() { + if (DEBUG) { + Log.d(TAG, getDebugLogString("collapse() called")); + } + + if (!readyToChangeState) return; + + final int height = getHeight(); + if (height == 0) { + setCurrentState(COLLAPSED); + return; + } + + if (currentAnimator != null && currentAnimator.isRunning()) currentAnimator.cancel(); + currentAnimator = AnimationUtils.animateHeight(this, ANIMATION_DURATION, 0); + + setCurrentState(COLLAPSED); + } + + public void expand() { + if (DEBUG) { + Log.d(TAG, getDebugLogString("expand() called")); + } + + if (!readyToChangeState) return; + + final int height = getHeight(); + if (height == this.targetHeight) { + setCurrentState(EXPANDED); + return; + } + + if (currentAnimator != null && currentAnimator.isRunning()) currentAnimator.cancel(); + currentAnimator = AnimationUtils.animateHeight(this, ANIMATION_DURATION, this.targetHeight); + setCurrentState(EXPANDED); + } + + public void switchState() { + if (!readyToChangeState) return; + + if (currentState == COLLAPSED) { + expand(); + } else { + collapse(); + } + } + + @ViewMode + public int getCurrentState() { + return currentState; + } + + public void setCurrentState(@ViewMode int currentState) { + this.currentState = currentState; + broadcastState(); + } + + public void broadcastState() { + for (StateListener listener : listeners) { + listener.onStateChanged(currentState); + } + } + + /** + * Add a listener which will be listening for changes in this view (i.e. collapsed or expanded). + */ + public void addListener(final StateListener listener) { + if (listeners.contains(listener)) { + throw new IllegalStateException("Trying to add the same listener multiple times"); + } + + listeners.add(listener); + } + + /** + * Remove a listener so it doesn't receive more state changes. + */ + public void removeListener(final StateListener listener) { + listeners.remove(listener); + } + + /** + * Simple interface used for listening state changes of the {@link CollapsibleView}. + */ + public interface StateListener { + /** + * Called when the state changes. + * + * @param newState the state that the {@link CollapsibleView} transitioned to,
+ * it's an integer being either {@link #COLLAPSED} or {@link #EXPANDED} + */ + void onStateChanged(@ViewMode int newState); + } + + /*////////////////////////////////////////////////////////////////////////// + // State Saving + //////////////////////////////////////////////////////////////////////////*/ + + @Nullable + @Override + public Parcelable onSaveInstanceState() { + return Icepick.saveInstanceState(this, super.onSaveInstanceState()); + } + + @Override + public void onRestoreInstanceState(Parcelable state) { + super.onRestoreInstanceState(Icepick.restoreInstanceState(this, state)); + + ready(); + } + + /*////////////////////////////////////////////////////////////////////////// + // Internal + //////////////////////////////////////////////////////////////////////////*/ + + public String getDebugLogString(String description) { + return String.format("%-100s → %s", + description, "readyToChangeState = [" + readyToChangeState + "], currentState = [" + currentState + "], targetHeight = [" + targetHeight + "]," + + " mW x mH = [" + getMeasuredWidth() + "x" + getMeasuredHeight() + "]" + + " W x H = [" + getWidth() + "x" + getHeight() + "]"); + } +} diff --git a/app/src/main/res/drawable-hdpi/ic_backup_black_24dp.png b/app/src/main/res/drawable-hdpi/ic_backup_black_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..e0938f1dcdac4942ff80fd40f08d8b9e02ab64c5 GIT binary patch literal 337 zcmV-X0j~auP) zArU3dA`rsk<@wZqqvV|aa4(l@$A3oCp2yyq`)=&1eNDg3B?sn0xA+0w$ z3c2l*knyuy3b<+_sEukNr)e@R=ua;0Bo(Ws6HVhei&^8nP%ArR7Y0LbeWlh`QkEdygxa-7A ji^9OtR9{cSj(?V)^cyV4tp{nL00000NkvXXu0mjfJ%W|) literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-hdpi/ic_backup_white_24dp.png b/app/src/main/res/drawable-hdpi/ic_backup_white_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..5e0b464cf17214e8ccf8595f898fed92be67f1ab GIT binary patch literal 345 zcmV-f0jB rWuGes_%)|k0wldT1B8K8mZytjNCo5Dsfv8ehCD3a)lUZh z+R)_t>x1pdu7fKz${ZTfC!AF9?iM-9?k*uvp|-dt%Sn&z(4Pg|k0wldT1B8K8v8Rh;h)3t!bJju)3OufXWlY|Q ziDC`h_2)hWZ@D)F2G-W_5)*1r=uZlf_zQbY2xrsZVK^@n_JGlrj2 Ve(jXz+X%Ft!PC{xWt~$(697ERKFDVd5(>BJ=niteU%2YFH`2sfB`Tj|9A4842*mXP-3vb zsu0uSBC~9)6z59Hybe0-MqLbL#b@u0ptMkcQbLaETaY~#3cp$;6z*8)gnH_w;WOw# z%6l2J9!gm+*FwFx8Ff%CZkiuMgSeTrKRaW+KmDuC42EYy@ILa0M5s>6LU8FOEP0!EMA1UJnE#X>H4|#tUDM{qm&kf2dMhR)ifj z8WUZ|7;_;tCIr>VOqfCg&;~EnCQz8@q32GR5=%FAqdn+F@^p=hssQl$U@002ovPDHLkV1jXsV!!|Z literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-mdpi/ic_backup_black_24dp.png b/app/src/main/res/drawable-mdpi/ic_backup_black_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..4cd6741c0b3e71f62e056b0154d2ffc7fb9e5400 GIT binary patch literal 226 zcmV<803H8{P)ggH;E5Qx83rS(;F0@EzGnJN#jhnGZuP6#?_8}2{7T0nZhJmEbIn{;# zz_m(_+!<~C`gH3M+U!XFgzVNOZSIBI8x77x!&6Fj^L~iJU9XR#ylt4D|l|t>qN(os04`0VQDb&v^2O_`qIXmm~999UT4m)0XEJ(`W8(m cgU(9$2OmuLn4T7UPyhe`07*qoM6N<$g1q)$+yDRo literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-mdpi/ic_backup_white_24dp.png b/app/src/main/res/drawable-mdpi/ic_backup_white_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..aa640629aa3896f2e18d858329f20b53ab21746d GIT binary patch literal 235 zcmVrJ(b+pF!*eGHa&9-%p|e5>p6)l9@$6v$yxlWK`3@rgGtqq98Q$Sl|-9kNqzpk l+F{I=dx09_X+q~(rTgsw&_Laz&Y5z<#jFVdQ&MBb@02Dzopr096P-!~g&Q literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-mdpi/ic_save_white_24dp.png b/app/src/main/res/drawable-mdpi/ic_save_white_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..015062ed3b316909362e3bfe43579715a4f2c14e GIT binary patch literal 168 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM0wlfaz7_+id`}n05R2ZcmmRqp6hvGD%@cNQ zTrf?MD{tY`DCrY!+h+!7)MX@<|M>91UCT6U&6_Q^vKIW^bNn|gNPwG~q2dAY{6@(2Ul)|NDzIgd>D5-Nv3R_&pT)_9!>hC0v RLqMw;JYD@<);T3K0RZw0JMaJi literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xhdpi/ic_backup_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_backup_black_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..81155da525b91830114fef98c251892fe81e40b7 GIT binary patch literal 386 zcmV-|0e$|7P)K5T(~PiE~6R#|+6jGNjAsVV3UDffiFJSYVqwI>wnQRm9*9e~=>} zLK3V37!xhh$ye_oiut6(B1Se5^}IPz!$4U*pPUGJ%cR-It_$ScO9rW4K!e$ z$n7J;mD_I$S$c>Qn`8iB%~7DpHyaM-5Bb3=A0wTwF;0v*E{R5T`yvLiY;nsAq1A9m ggMbnlJRZ*gUuq}bb_}IlRR91007*qoM6N<$f_R^x2LJ#7 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xhdpi/ic_backup_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_backup_white_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..a9602d11b8d4e33f91367f9676eebce86d88c117 GIT binary patch literal 405 zcmV;G0c!qYU6oB!gw#7w6AHZ6C8lfO9C{)|G5Tytb2Nyx8_-E0{MU0E9)CbTPaFR+9g|>FG zON!-l$YQ|cp2S{o$amIoesFUk5nX9%X-u=i8ISb%Vn~+#XgXyo36S9yZ8t$e2V4-Rlej>QxUI$k=IIl+4l^2Hi5>3g5tf5P zSi2^mfXj%e90Um^I%K1O8Urw|k0h4Efi2LiHxP|5psHJMBs`wDq(mkN6x4@fCj6c$ z{sdh0dOZOLO!z=Sb8`|Ahdxj@uTM;r5TR*YpPXou72eS$!9iC=7+M8f+rW$PW)<*+ zrfr~Z6=(^IjO@N-;o9ys74i(QQ=6P12pia`j0pa0GX9Q&pUibW8D|Y9gMhHW6{9ho zeL3v6CC@f@yyB50j}|T!a!3%cI4O_<{D0sZ6}?4(_}0Fh00000NkvXXu0mjfY7nTZ literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xhdpi/ic_import_export_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_import_export_black_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..9b643bd3bcfa4340046a42fb4f41a751746653ad GIT binary patch literal 186 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA0wn)(8}b0DMo$;VkP61P*LLz9cHnTm`1|?h zij(@gH+;FIkkUJe?@~)~bsYE7KSvf!&QLm{aI$TUXOpb$EsnAUOXoVM|GMBfLHySx z&xT`9Pwv{|rV;ruYXMuz-@^{PR~l9~N^xA=VH>&qdQ$w!t&+ca*7L?3JECyIq2Ed` ldC}x0>hr#L@-2N*&Fq-{?t4VOtU1sz44$rjF6*2UngC=SOwIrR literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xhdpi/ic_import_export_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_import_export_white_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..e22e18866cccd3dbf95abf46905f7b594d727f31 GIT binary patch literal 202 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA0wn)(8}b0DiJmTwArXh)PP67~P!M6cf0j{n z@21w1w-~f8=FRUcZ0}22_F=NA@FUg<0=8e785AmCx`(jw|7|f=j-G$f*zl|IyAwKp zo7B_V>=JU97u{ibH(BFHEj!mAa|dqi1qaxfezj^G;8jS-2Fe*6JCN!1ZNsuu$-}m1 z3Tq}OZx8JeNT@iK_=r{&XS3j3^P6^5To~qfUqA&PMF*F<|kx;Md$&W&;x!#4;TRufSvnyKmY=;&`=O< zD`{EPz!xd^WlTz7A;m7|q5~RSbvmHHRiy(WT*Yq$l(?#OK#Qwh2dre+9fV)gZ4fyZ>#A&pWX|cF@nuJ>{9{^+CL!nen`MaoS0vDoPmT;NZb{N*U=t-36Ko{ O0000M1L^E>wD z_G`*BEKt%ZU=9!xlW_2kcwqhg@J~+L1PJQf&&tujU~xgSfq^Aq(NdMD@Qa448^br9 zJv*meXvT?xG}dmujKXdc5cH-b0=D=O1`+&;6cz{l^s*6%`c~ZPDYR2j2K$*^=*G z8+XBeo7K2x2aQ`4R$1AYUX5x}qkiv&h`n=c{ZYHU5X5iBF6&mwSC@(( zQ04C0AaR>a2pRTBYuc-NC}iAy&60IpNU$ps)h8s{0SVgbTQbms4w)Mf5^h-4?Qp`V z8NY;x4H^_8wq&O@*QC#s5I=(uNxEu{b$KOJ!HFgPv{7_zMue(4v82Ny+h#f=Cyq24 zd)~7NI5DJ;B5L(45hsQ;Ys@)&G$eT2L~!CS?yXZ=RHiF2C!Q31&{gr?$#Cc?NWpm} zD>|B$lYn$%CFx0aP6E;;fX+0W1f(9|WExHa(kJb3I~6AZ=?pv*BpU@ zZWeO|8p<1JGH;LUGc&mYmARES(D7V>=IPAamzu}%~aPY;Jx!nt**mE;azb zwH$Efv;)xMY287+{;0#tx^gb7sKZpcNMDmi4LCYS{S z+C{cqzni9gQBKS#n>T4lwPNf literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/ic_backup_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_backup_white_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..3ff57ad3e897a332dc77086a5d3fdb9ed0c40b41 GIT binary patch literal 589 zcmV-T0Bp}VWfd*+9a9l|5T|oUj(Q*`|2D6-HH+>yJ`n#hb#l7Yv#lL|nHXH>B-ck7z zl-E6UGZLI5A_j59nxi0HBZ@(f>^Tb3#owSIdybMc@(XlA$+0JWpo9oF)f{`$X+(I6 zzD@vbO$x$ik3cXita=5S@C~%!+mU?~>Ry3b!VTX*$Gifq2t&SIYT=<@&t5p}*9W5z z(eUQ0BQFTTUi@f0{*ix$>|3T3gNi&R>#9f$63RR$%O=7;d zP5E^`jVV%HQ(HM$i*!srpE8M;?e6HPn|Ly0h2He*io8Fw_lhlN$(-EItD7@@|IM(* z;D(YD_Br82%WF0*7E_;EGP!h**57GSAkRVsh3`PXDmxSYTv%pI=5wm-9sJqnOrFogIiPnmusjg bEmB{gCV4q2c*1p{!x=nX{an^LB{Ts56RcS| literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/ic_import_export_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_import_export_white_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..33c21c5c46394223f907dd5af09756dd3f86c149 GIT binary patch literal 252 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY0wn)GsXhaw4tu&dhGg7(d()fiumO+j#WyzR zwnx8P@WIQQ%Wa3}f{>sGas0NH%>vC=N?lyORBLI?v}le?Rm{AyTv1$|*>iGEvPyc` zYE5zVt|hM3XYb7kb=Tc#F;C@g$+oM_mtW`k&E5N2+3x)+8Gm*KhX#gNh9_NYEF1zr zc0~gtBNNLDe&yNimZjg$C8u1L5U)R--BUB^iRD!@<9}iIivKeQ9g#|E7uhO$Gc0_Y z&`*A;phBr78Ruq)1QlupiJfyhbwp8VSDybKP4B(@ACBJydWXT&)z4*}Q$iB}uv=m# literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/ic_save_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_save_black_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..871291b4e6b3aab1840613e5c6a8a498a972649b GIT binary patch literal 368 zcmV-$0gwKPP)^c^fo|P|pSPy$;Uv zZr}&^4i28@d0tw9F;9FD|7rIeRgE{IJrya~94%=~w5KF3iRVT7B%T*3{@O9wBt6I` z=|MJ053)&mkWJEqY*KzubdZA_bmHX)Imkf{a*%@@v}Hn%Iu!~U^qJ-bt!VSN4rMOT z1DEWo$|477wTJUVD%GGZZ4T8{gC>V~KWl~mc+6>B{xvY@($^O)=WDCzEataaXtamIiqfk2BP^HUsXUqsP1 zB=sT#ElKK51{#sn1uF^Xm4d`-ybCzjkY3=HXErDL%1cHRc%J8ZDSZQI<(51+Brs$E O0000Xo3#U&Y*@0w|JqSu;kW8`%L1_GX^wikkMku z+!nN<`{#6z$_n(rC9(4bD+|yj&iNr#+MqpM!jbj0K{JA|g)V3yf=0TarU+{5f*K+y z*9B$&4b&1r9bM2+1dVk;a}l)E1wrp9=N7a<6~#f$6D?x75&I%ZEatS z3r4ib$>}g=S;qtI3$1o(pck}Sr-AO#YLW)JV1q`lWa+;+-xWC5gj7%<=ZDs^(9wc2%hE=pP1M4*1VKb(xCjD8NL;uJ0+HyG*-S8?s}|OXZmMl0iiL}U z0ud~Vp$}tP)*^8cGL_Oep)$_fPBcH~o^$5j1LyZ&3-5ECnTz8%j^j9v<2a5s6Vw`6$0d|yIyd{Thu^bazqmIC7;yKI64wvH@jReLyl8A@B;S9d3 z<0+!BA!_hZ2Xlzxe$$38%6W)rT0dnJ*?uMvjT@taB09tuMDr#&SiGm7LJaH!6_`gk z6NsUWQ;Knq5Q9r#SO;QwmoVrEbH)SGRAbC2g82<%NF!3DokFWRI_PJNS)|~_iF^2~ z*ZN*qH~ENoUL3$v!f74PeSp7+^7-BpMo?159p(}B^F1fzsKK*(CJ{a7DRE<87cvwUG01y6M6xlPvv}H6Beiec!g-)c0}X4 z0lrxr@SPCMS{xvpge9v3qP7Q&z%Q!VQQ^*dEYsbwC;p+x{hn_Z9~X z16;NHMFv%5tqo`ZfcusPOc4TrTIQ_`=wU?^v2F`QSP|y4bpd^>ae@qD(PpURKP@a< z6wt~BuGsqVS(RYLe_@moHr33Zz@NXHEgfev&>x->wiV_!^MU;3X=TSE^pXwKYb!w| z1*)QpBzeT=P0>RH&#GzT9xwPxnnk2YTO>`2VWKn;VjahE9LI4S$8ns0bEkQc(oxfv P00000NkvXXu0mjf7#&5* literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/ic_backup_white_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_backup_white_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..2180f73e8d7d300cddc7ae2c2a071ee58ddd4260 GIT binary patch literal 770 zcmV+d1O5DoP) z8s{2&w+58)!OVOi0;Gttdvidqnfamwn4@K5K-kdNmONpNK0^NjMg-jC>n z3(S`!j1lqzt_sYTCfxP{VgmE!3FrO*8bs#H6Bek-0Xl@{%M%82fFYsz@`McaD8&nm zw(V4t^`R7BECzffq)1u}fMbdks{uW>111#Ttp>#H{t(0eisx1XmKB4x13Ih*Boqg1 z{}3zQS_~LhC@$OmMW(1BZ7rZFchkF;0_F(iZrZ?-m4N=g54L)+uMA=Dqs5Qd*3kNg zyGKc3P2PX%y=OD4*i+j^2OO7d`t|Vwyo6PP-y|iQ0TiwL4E+3ETOYF?VV2-FM9KCC z-DTWliQu(B+s3GsGE{>_;=bLPzx9tFa#PleK6r3*LeUOD{i&GX}e zVXHQ_PB43wqRseL# nC+XYgzi-zyHa6Z|f6Y8^((bn>+9SgTe~DWM4fgX4RM literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/ic_import_export_white_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_import_export_white_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..a5e55a4707c286e7068434f14468725902469e46 GIT binary patch literal 328 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD0wg^q?%&M7z{v0E;uunK>+S5bTulKIthL3a zpSNGnz4*>0QK4w+YS$X}v^%?I-0XO?)0GjZi6MTG*VN5*YsJ@b?t0<;cE$aSlSiZP z>pF%n&4~FDom*IX;r$l(*K6f%9rqT!SoDhj)YASH_OCdax7Tf9@wGZE;rf5sW*@;X z-ghTopY@W!_+)9|v31(NuDy0~+OqQ2xosCy-&ZIozKSVMR(NePUV8g{Lgyfrfc(s=X?cuujvCzP}U63Uj2zrL7w$= L^>bP0l+XkK5r~T2 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/ic_save_black_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_save_black_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..ba001835a93dd223533442cd94bfd8582370b7ef GIT binary patch literal 477 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD0wg^q?%&M7z_`TI#WAFU@$HSkEU7?|_K*Io z%NLa~J`(HN!EsQwqoev|FpI8-r`Pj^>lItxES#dJw6yYM$|I#OM=zHzyti}Pwc^wN zt))Mh%cSoRUpghoOS5!|qNV?v4aVoI&jobrM2 z$mu=)$jDq&z1ekBu@?lqI?!8Eh|s}IjDKfvy^)|NSsGyL&A?F9>b zr|#PGi^XTftux9GW?F@2e|W@S;OgUD-ymq%`c2|5CSP&+_V&O1U75EBlQW zA57$ZmUC%mSY_hL8FrGAwa3xp#U)%C$H2?HsTP{@eO+0R%w9&s6jPrjS{Klkz&V60~+kL@gb&1nDt literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/ic_save_white_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_save_white_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..bd80bf1f74f5dee36deef2d8fefbd1b59b2aa1f7 GIT binary patch literal 504 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD0wg^q?%&M7z_{Pj#WAFU@$I#EFX=#uwvXpI zW{Pd)=D69^(an*rvIc=k>%>fl|gI;!tRK{Z;84l|>e7(Vy4L%kU(lfKjiFI>oQ3X>ech^2hYjIl+l`2ky&>*QB4^6 + + + + + + + + +