package org.schabi.newpipe.settings; import android.annotation.SuppressLint; import android.content.Context; import android.content.SharedPreferences; import android.os.Bundle; import android.text.InputType; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; import android.widget.ProgressBar; import android.widget.RadioButton; import android.widget.TextView; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; import androidx.appcompat.widget.AppCompatImageView; import androidx.fragment.app.Fragment; import androidx.preference.PreferenceManager; import androidx.recyclerview.widget.ItemTouchHelper; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import com.google.android.material.floatingactionbutton.FloatingActionButton; import com.grack.nanojson.JsonStringWriter; import com.grack.nanojson.JsonWriter; import org.schabi.newpipe.R; import org.schabi.newpipe.databinding.DialogEditTextBinding; import org.schabi.newpipe.extractor.services.peertube.PeertubeInstance; import org.schabi.newpipe.util.Constants; import org.schabi.newpipe.util.PeertubeHelper; import org.schabi.newpipe.util.ThemeHelper; import java.util.ArrayList; import java.util.Collections; import java.util.List; import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; import io.reactivex.rxjava3.core.Single; import io.reactivex.rxjava3.disposables.CompositeDisposable; import io.reactivex.rxjava3.disposables.Disposable; import io.reactivex.rxjava3.schedulers.Schedulers; public class PeertubeInstanceListFragment extends Fragment { private final List instanceList = new ArrayList<>(); private PeertubeInstance selectedInstance; private String savedInstanceListKey; private InstanceListAdapter instanceListAdapter; private ProgressBar progressBar; private SharedPreferences sharedPreferences; private CompositeDisposable disposables = new CompositeDisposable(); /*////////////////////////////////////////////////////////////////////////// // Lifecycle //////////////////////////////////////////////////////////////////////////*/ @Override public void onCreate(@Nullable final Bundle savedInstanceState) { super.onCreate(savedInstanceState); sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext()); savedInstanceListKey = getString(R.string.peertube_instance_list_key); selectedInstance = PeertubeHelper.getCurrentInstance(); updateInstanceList(); setHasOptionsMenu(true); } @Override public View onCreateView(@NonNull final LayoutInflater inflater, final ViewGroup container, final Bundle savedInstanceState) { return inflater.inflate(R.layout.fragment_instance_list, container, false); } @Override public void onViewCreated(@NonNull final View rootView, @Nullable final Bundle savedInstanceState) { super.onViewCreated(rootView, savedInstanceState); initViews(rootView); } private void initViews(@NonNull final View rootView) { final TextView instanceHelpTV = rootView.findViewById(R.id.instanceHelpTV); instanceHelpTV.setText(getString(R.string.peertube_instance_url_help, getString(R.string.peertube_instance_list_url))); initButton(rootView); final RecyclerView listInstances = rootView.findViewById(R.id.instances); listInstances.setLayoutManager(new LinearLayoutManager(requireContext())); final ItemTouchHelper itemTouchHelper = new ItemTouchHelper(getItemTouchCallback()); itemTouchHelper.attachToRecyclerView(listInstances); instanceListAdapter = new InstanceListAdapter(requireContext(), itemTouchHelper); listInstances.setAdapter(instanceListAdapter); progressBar = rootView.findViewById(R.id.loading_progress_bar); } @Override public void onResume() { super.onResume(); ThemeHelper.setTitleToAppCompatActivity(getActivity(), getString(R.string.peertube_instance_url_title)); } @Override public void onPause() { super.onPause(); saveChanges(); } @Override public void onDestroy() { super.onDestroy(); if (disposables != null) { disposables.clear(); } disposables = null; } /*////////////////////////////////////////////////////////////////////////// // Menu //////////////////////////////////////////////////////////////////////////*/ @Override public void onCreateOptionsMenu(@NonNull final Menu menu, @NonNull final MenuInflater inflater) { super.onCreateOptionsMenu(menu, inflater); inflater.inflate(R.menu.menu_chooser_fragment, menu); } @Override public boolean onOptionsItemSelected(final MenuItem item) { if (item.getItemId() == R.id.menu_item_restore_default) { restoreDefaults(); return true; } return super.onOptionsItemSelected(item); } /*////////////////////////////////////////////////////////////////////////// // Utils //////////////////////////////////////////////////////////////////////////*/ private void updateInstanceList() { instanceList.clear(); instanceList.addAll(PeertubeHelper.getInstanceList(requireContext())); } private void selectInstance(final PeertubeInstance instance) { selectedInstance = PeertubeHelper.selectInstance(instance, requireContext()); sharedPreferences.edit().putBoolean(Constants.KEY_MAIN_PAGE_CHANGE, true).apply(); } private void saveChanges() { final JsonStringWriter jsonWriter = JsonWriter.string().object().array("instances"); for (final PeertubeInstance instance : instanceList) { jsonWriter.object(); jsonWriter.value("name", instance.getName()); jsonWriter.value("url", instance.getUrl()); jsonWriter.end(); } final String jsonToSave = jsonWriter.end().end().done(); sharedPreferences.edit().putString(savedInstanceListKey, jsonToSave).apply(); } private void restoreDefaults() { new AlertDialog.Builder(requireContext()) .setTitle(R.string.restore_defaults) .setMessage(R.string.restore_defaults_confirmation) .setNegativeButton(R.string.cancel, null) .setPositiveButton(R.string.ok, (dialog, which) -> { sharedPreferences.edit().remove(savedInstanceListKey).apply(); selectInstance(PeertubeInstance.DEFAULT_INSTANCE); updateInstanceList(); instanceListAdapter.notifyDataSetChanged(); }) .show(); } private void initButton(final View rootView) { final FloatingActionButton fab = rootView.findViewById(R.id.addInstanceButton); fab.setOnClickListener(v -> showAddItemDialog(requireContext())); } private void showAddItemDialog(final Context c) { final DialogEditTextBinding dialogBinding = DialogEditTextBinding.inflate(getLayoutInflater()); dialogBinding.dialogEditText.setInputType( InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_URI); dialogBinding.dialogEditText.setHint(R.string.peertube_instance_add_help); new AlertDialog.Builder(c) .setTitle(R.string.peertube_instance_add_title) .setIcon(R.drawable.ic_placeholder_peertube) .setView(dialogBinding.getRoot()) .setNegativeButton(R.string.cancel, null) .setPositiveButton(R.string.ok, (dialog1, which) -> { final String url = dialogBinding.dialogEditText.getText().toString(); addInstance(url); }) .show(); } private void addInstance(final String url) { final String cleanUrl = cleanUrl(url); if (cleanUrl == null) { return; } progressBar.setVisibility(View.VISIBLE); final Disposable disposable = Single.fromCallable(() -> { final PeertubeInstance instance = new PeertubeInstance(cleanUrl); instance.fetchInstanceMetaData(); return instance; }).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()) .subscribe((instance) -> { progressBar.setVisibility(View.GONE); add(instance); }, e -> { progressBar.setVisibility(View.GONE); Toast.makeText(getActivity(), R.string.peertube_instance_add_fail, Toast.LENGTH_SHORT).show(); }); disposables.add(disposable); } @Nullable private String cleanUrl(final String url) { String cleanUrl = url.trim(); // if protocol not present, add https if (!cleanUrl.startsWith("http")) { cleanUrl = "https://" + cleanUrl; } // remove trailing slash cleanUrl = cleanUrl.replaceAll("/$", ""); // only allow https if (!cleanUrl.startsWith("https://")) { Toast.makeText(getActivity(), R.string.peertube_instance_add_https_only, Toast.LENGTH_SHORT).show(); return null; } // only allow if not already exists for (final PeertubeInstance instance : instanceList) { if (instance.getUrl().equals(cleanUrl)) { Toast.makeText(getActivity(), R.string.peertube_instance_add_exists, Toast.LENGTH_SHORT).show(); return null; } } return cleanUrl; } private void add(final PeertubeInstance instance) { instanceList.add(instance); instanceListAdapter.notifyDataSetChanged(); } private ItemTouchHelper.SimpleCallback getItemTouchCallback() { return new ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP | ItemTouchHelper.DOWN, ItemTouchHelper.START | ItemTouchHelper.END) { @Override public int interpolateOutOfBoundsScroll(@NonNull final RecyclerView recyclerView, final int viewSize, final int viewSizeOutOfBounds, final int totalSize, final long msSinceStartScroll) { final int standardSpeed = super.interpolateOutOfBoundsScroll(recyclerView, viewSize, viewSizeOutOfBounds, totalSize, msSinceStartScroll); final int minimumAbsVelocity = Math.max(12, Math.abs(standardSpeed)); return minimumAbsVelocity * (int) Math.signum(viewSizeOutOfBounds); } @Override public boolean onMove(@NonNull final RecyclerView recyclerView, @NonNull final RecyclerView.ViewHolder source, @NonNull final RecyclerView.ViewHolder target) { if (source.getItemViewType() != target.getItemViewType() || instanceListAdapter == null) { return false; } final int sourceIndex = source.getBindingAdapterPosition(); final int targetIndex = target.getBindingAdapterPosition(); instanceListAdapter.swapItems(sourceIndex, targetIndex); return true; } @Override public boolean isLongPressDragEnabled() { return false; } @Override public boolean isItemViewSwipeEnabled() { return true; } @Override public void onSwiped(@NonNull final RecyclerView.ViewHolder viewHolder, final int swipeDir) { final int position = viewHolder.getBindingAdapterPosition(); // do not allow swiping the selected instance if (instanceList.get(position).getUrl().equals(selectedInstance.getUrl())) { instanceListAdapter.notifyItemChanged(position); return; } instanceList.remove(position); instanceListAdapter.notifyItemRemoved(position); if (instanceList.isEmpty()) { instanceList.add(selectedInstance); instanceListAdapter.notifyItemInserted(0); } } }; } /*////////////////////////////////////////////////////////////////////////// // List Handling //////////////////////////////////////////////////////////////////////////*/ private class InstanceListAdapter extends RecyclerView.Adapter { private final LayoutInflater inflater; private final ItemTouchHelper itemTouchHelper; private RadioButton lastChecked; InstanceListAdapter(final Context context, final ItemTouchHelper itemTouchHelper) { this.itemTouchHelper = itemTouchHelper; this.inflater = LayoutInflater.from(context); } public void swapItems(final int fromPosition, final int toPosition) { Collections.swap(instanceList, fromPosition, toPosition); notifyItemMoved(fromPosition, toPosition); } @NonNull @Override public InstanceListAdapter.TabViewHolder onCreateViewHolder(@NonNull final ViewGroup parent, final int viewType) { final View view = inflater.inflate(R.layout.item_instance, parent, false); return new InstanceListAdapter.TabViewHolder(view); } @Override public void onBindViewHolder(@NonNull final InstanceListAdapter.TabViewHolder holder, final int position) { holder.bind(position, holder); } @Override public int getItemCount() { return instanceList.size(); } class TabViewHolder extends RecyclerView.ViewHolder { private final AppCompatImageView instanceIconView; private final TextView instanceNameView; private final TextView instanceUrlView; private final RadioButton instanceRB; private final ImageView handle; TabViewHolder(final View itemView) { super(itemView); instanceIconView = itemView.findViewById(R.id.instanceIcon); instanceNameView = itemView.findViewById(R.id.instanceName); instanceUrlView = itemView.findViewById(R.id.instanceUrl); instanceRB = itemView.findViewById(R.id.selectInstanceRB); handle = itemView.findViewById(R.id.handle); } @SuppressLint("ClickableViewAccessibility") void bind(final int position, final TabViewHolder holder) { handle.setOnTouchListener(getOnTouchListener(holder)); final PeertubeInstance instance = instanceList.get(position); instanceNameView.setText(instance.getName()); instanceUrlView.setText(instance.getUrl()); instanceRB.setOnCheckedChangeListener(null); if (selectedInstance.getUrl().equals(instance.getUrl())) { if (lastChecked != null && lastChecked != instanceRB) { lastChecked.setChecked(false); } instanceRB.setChecked(true); lastChecked = instanceRB; } instanceRB.setOnCheckedChangeListener((buttonView, isChecked) -> { if (isChecked) { selectInstance(instance); if (lastChecked != null && lastChecked != instanceRB) { lastChecked.setChecked(false); } lastChecked = instanceRB; } }); instanceIconView.setImageResource(R.drawable.ic_placeholder_peertube); } @SuppressLint("ClickableViewAccessibility") private View.OnTouchListener getOnTouchListener(final RecyclerView.ViewHolder item) { return (view, motionEvent) -> { if (motionEvent.getActionMasked() == MotionEvent.ACTION_DOWN) { if (itemTouchHelper != null && getItemCount() > 1) { itemTouchHelper.startDrag(item); return true; } } return false; }; } } } }