From 527c38adf9ac1c43cd33dd371f7e37efaecb4e9f Mon Sep 17 00:00:00 2001
From: yausername <13ritvik@gmail.com>
Date: Sun, 24 Nov 2019 21:08:06 +0530
Subject: [PATCH] easily switch between multiple peertube instances

---
 app/build.gradle                              |   2 +-
 .../java/org/schabi/newpipe/MainActivity.java |  57 ++-
 .../settings/ContentSettingsFragment.java     |  46 +-
 .../PeertubeInstanceListFragment.java         | 418 ++++++++++++++++++
 .../schabi/newpipe/util/PeertubeHelper.java   |  65 +++
 .../schabi/newpipe/util/ServiceHelper.java    |  24 +-
 .../org/schabi/newpipe/util/ThemeHelper.java  |   7 +-
 .../res/layout/fragment_instance_list.xml     |  51 +++
 .../main/res/layout/instance_spinner_item.xml |   6 +
 .../res/layout/instance_spinner_layout.xml    |   9 +
 app/src/main/res/layout/item_instance.xml     |  83 ++++
 app/src/main/res/values/settings_keys.xml     |   5 +-
 app/src/main/res/values/strings.xml           |   7 +-
 app/src/main/res/xml/content_settings.xml     |   8 +-
 14 files changed, 724 insertions(+), 64 deletions(-)
 create mode 100644 app/src/main/java/org/schabi/newpipe/settings/PeertubeInstanceListFragment.java
 create mode 100644 app/src/main/java/org/schabi/newpipe/util/PeertubeHelper.java
 create mode 100644 app/src/main/res/layout/fragment_instance_list.xml
 create mode 100644 app/src/main/res/layout/instance_spinner_item.xml
 create mode 100644 app/src/main/res/layout/instance_spinner_layout.xml
 create mode 100644 app/src/main/res/layout/item_instance.xml

diff --git a/app/build.gradle b/app/build.gradle
index 4259d45a2..a128d8841 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -62,7 +62,7 @@ dependencies {
         exclude module: 'support-annotations'
     })
 
-    implementation 'com.github.yausername:NewPipeExtractor:bc75c66'
+    implementation 'com.github.yausername:NewPipeExtractor:6a7680c'
 
     testImplementation 'junit:junit:4.12'
     testImplementation 'org.mockito:mockito-core:2.23.0'
diff --git a/app/src/main/java/org/schabi/newpipe/MainActivity.java b/app/src/main/java/org/schabi/newpipe/MainActivity.java
index 82d4e4063..927fc1589 100644
--- a/app/src/main/java/org/schabi/newpipe/MainActivity.java
+++ b/app/src/main/java/org/schabi/newpipe/MainActivity.java
@@ -29,14 +29,18 @@ import android.os.Handler;
 import android.os.Looper;
 import android.preference.PreferenceManager;
 import android.util.Log;
+import android.view.LayoutInflater;
 import android.view.Menu;
 import android.view.MenuInflater;
 import android.view.MenuItem;
 import android.view.View;
 import android.view.Window;
 import android.view.WindowManager;
+import android.widget.AdapterView;
+import android.widget.ArrayAdapter;
 import android.widget.Button;
 import android.widget.ImageView;
+import android.widget.Spinner;
 import android.widget.TextView;
 
 import androidx.annotation.NonNull;
@@ -47,12 +51,15 @@ import androidx.appcompat.widget.Toolbar;
 import androidx.core.view.GravityCompat;
 import androidx.drawerlayout.widget.DrawerLayout;
 import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentManager;
 
 import com.google.android.material.navigation.NavigationView;
 
 import org.schabi.newpipe.extractor.NewPipe;
+import org.schabi.newpipe.extractor.ServiceList;
 import org.schabi.newpipe.extractor.StreamingService;
 import org.schabi.newpipe.extractor.exceptions.ExtractionException;
+import org.schabi.newpipe.extractor.services.peertube.PeertubeInstance;
 import org.schabi.newpipe.fragments.BackPressable;
 import org.schabi.newpipe.fragments.MainFragment;
 import org.schabi.newpipe.fragments.detail.VideoDetailFragment;
@@ -61,11 +68,15 @@ import org.schabi.newpipe.report.ErrorActivity;
 import org.schabi.newpipe.util.Constants;
 import org.schabi.newpipe.util.KioskTranslator;
 import org.schabi.newpipe.util.NavigationHelper;
+import org.schabi.newpipe.util.PeertubeHelper;
 import org.schabi.newpipe.util.PermissionHelper;
 import org.schabi.newpipe.util.ServiceHelper;
 import org.schabi.newpipe.util.StateSaver;
 import org.schabi.newpipe.util.ThemeHelper;
 
+import java.util.ArrayList;
+import java.util.List;
+
 public class MainActivity extends AppCompatActivity {
     private static final String TAG = "MainActivity";
     public static final boolean DEBUG = !BuildConfig.BUILD_TYPE.equals("release");
@@ -300,13 +311,57 @@ public class MainActivity extends AppCompatActivity {
             final String title = s.getServiceInfo().getName() +
                     (ServiceHelper.isBeta(s) ? " (beta)" : "");
 
-            drawerItems.getMenu()
+            MenuItem menuItem = drawerItems.getMenu()
                     .add(R.id.menu_services_group, s.getServiceId(), ORDER, title)
                     .setIcon(ServiceHelper.getIcon(s.getServiceId()));
+
+            // peertube specifics
+            if(s.getServiceId() == 3){
+                enhancePeertubeMenu(s, menuItem);
+            }
         }
         drawerItems.getMenu().getItem(ServiceHelper.getSelectedServiceId(this)).setChecked(true);
     }
 
+    private void enhancePeertubeMenu(StreamingService s, MenuItem menuItem) {
+        PeertubeInstance currentInstace = PeertubeHelper.getCurrentInstance();
+        menuItem.setTitle(currentInstace.getName() + (ServiceHelper.isBeta(s) ? " (beta)" : ""));
+        Spinner spinner = (Spinner) LayoutInflater.from(this).inflate(R.layout.instance_spinner_layout, null);
+        List<PeertubeInstance> instances = PeertubeHelper.getInstanceList(this);
+        List<String> items = new ArrayList<>();
+        int defaultSelect = 0;
+        for(PeertubeInstance instance: instances){
+            items.add(instance.getName());
+            if(instance.getUrl().equals(currentInstace.getUrl())){
+                defaultSelect = items.size()-1;
+            }
+        }
+        ArrayAdapter<String> adapter = new ArrayAdapter<>(this, R.layout.instance_spinner_item, items);
+        adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+        spinner.setAdapter(adapter);
+        spinner.setSelection(defaultSelect, false);
+        spinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
+            @Override
+            public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
+                PeertubeInstance newInstance = instances.get(position);
+                if(newInstance.getUrl().equals(PeertubeHelper.getCurrentInstance().getUrl())) return;
+                PeertubeHelper.selectInstance(newInstance, getApplicationContext());
+                changeService(menuItem);
+                drawer.closeDrawers();
+                new Handler(Looper.getMainLooper()).postDelayed(() -> {
+                    getSupportFragmentManager().popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE);
+                    recreate();
+                }, 300);
+            }
+
+            @Override
+            public void onNothingSelected(AdapterView<?> parent) {
+
+            }
+        });
+        menuItem.setActionView(spinner);
+    }
+
     private void showTabs() throws ExtractionException {
         serviceArrow.setImageResource(R.drawable.ic_arrow_down_white);
 
diff --git a/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java
index dd40f0d60..67098964d 100644
--- a/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java
+++ b/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java
@@ -12,23 +12,18 @@ import android.widget.Toast;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
-import androidx.preference.EditTextPreference;
 import androidx.preference.Preference;
 
 import com.nononsenseapps.filepicker.Utils;
 import com.nostra13.universalimageloader.core.ImageLoader;
 
-import org.schabi.newpipe.App;
 import org.schabi.newpipe.R;
 import org.schabi.newpipe.extractor.NewPipe;
-import org.schabi.newpipe.extractor.ServiceList;
 import org.schabi.newpipe.extractor.localization.ContentCountry;
 import org.schabi.newpipe.extractor.localization.Localization;
-import org.schabi.newpipe.extractor.services.peertube.PeertubeInstance;
 import org.schabi.newpipe.report.ErrorActivity;
 import org.schabi.newpipe.report.UserAction;
 import org.schabi.newpipe.util.FilePickerActivityHelper;
-import org.schabi.newpipe.util.NavigationHelper;
 import org.schabi.newpipe.util.ZipHelper;
 
 import java.io.BufferedOutputStream;
@@ -46,11 +41,6 @@ import java.util.Map;
 import java.util.zip.ZipFile;
 import java.util.zip.ZipOutputStream;
 
-import io.reactivex.Completable;
-import io.reactivex.android.schedulers.AndroidSchedulers;
-import io.reactivex.disposables.Disposable;
-import io.reactivex.schedulers.Schedulers;
-
 public class ContentSettingsFragment extends BasePreferenceFragment {
 
     private static final int REQUEST_IMPORT_PATH = 8945;
@@ -127,41 +117,7 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
             return true;
         });
 
-        Preference peerTubeInstance = findPreference(getString(R.string.peertube_instance_url_key));
-        SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getContext());
-        peerTubeInstance.setDefaultValue(sharedPreferences.getString(getString(R.string.peertube_instance_url_key), ServiceList.PeerTube.getBaseUrl()));
-        peerTubeInstance.setSummary(sharedPreferences.getString(getString(R.string.peertube_instance_url_key), ServiceList.PeerTube.getBaseUrl()));
-
-        peerTubeInstance.setOnPreferenceChangeListener((Preference p, Object newInstance) -> {
-            EditTextPreference pEt = (EditTextPreference) p;
-            String url = (String) newInstance;
-            if (!url.startsWith("https://")) {
-                Toast.makeText(getActivity(), "instance url should start with https://",
-                        Toast.LENGTH_SHORT).show();
-            } else {
-                pEt.setSummary("fetching instance details..");
-                Disposable disposable = Completable.fromAction(() -> {
-                    PeertubeInstance instance = new PeertubeInstance(url);
-                    instance.fetchInstanceMetaData();
-                    ServiceList.PeerTube.setInstance(instance);
-                }).subscribeOn(Schedulers.io())
-                        .observeOn(AndroidSchedulers.mainThread())
-                        .subscribe(() -> {
-                            pEt.setSummary(url);
-                            pEt.setText(url);
-                            SharedPreferences.Editor editor = sharedPreferences.edit();
-                            editor.putString(App.getApp().getString(R.string.peertube_instance_name_key), ServiceList.PeerTube.getServiceInfo().getName()).apply();
-                            editor.putString(App.getApp().getString(R.string.current_service_key), ServiceList.PeerTube.getServiceInfo().getName()).apply();
-                            NavigationHelper.openMainActivity(App.getApp());
-                        }, error -> {
-                            pEt.setSummary(ServiceList.PeerTube.getBaseUrl());
-                            Toast.makeText(getActivity(), "unable to update instance",
-                                    Toast.LENGTH_SHORT).show();
-                        });
-            }
-            return false;
-        });
-    }
+   }
 
     @Override
     public void onDestroy() {
diff --git a/app/src/main/java/org/schabi/newpipe/settings/PeertubeInstanceListFragment.java b/app/src/main/java/org/schabi/newpipe/settings/PeertubeInstanceListFragment.java
new file mode 100644
index 000000000..097d96d20
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/settings/PeertubeInstanceListFragment.java
@@ -0,0 +1,418 @@
+package org.schabi.newpipe.settings;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.os.Bundle;
+import android.preference.PreferenceManager;
+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.EditText;
+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.ActionBar;
+import androidx.appcompat.app.AlertDialog;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.appcompat.content.res.AppCompatResources;
+import androidx.appcompat.widget.AppCompatImageView;
+import androidx.fragment.app.Fragment;
+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.extractor.ServiceList;
+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.Single;
+import io.reactivex.android.schedulers.AndroidSchedulers;
+import io.reactivex.disposables.CompositeDisposable;
+import io.reactivex.disposables.Disposable;
+import io.reactivex.schedulers.Schedulers;
+
+public class PeertubeInstanceListFragment extends Fragment {
+
+    private List<PeertubeInstance> instanceList = new ArrayList<>();
+    private PeertubeInstance selectedInstance;
+    private String savedInstanceListKey;
+    public InstanceListAdapter instanceListAdapter;
+
+    private ProgressBar progressBar;
+    private SharedPreferences sharedPreferences;
+
+    private CompositeDisposable disposables = new CompositeDisposable();
+
+    /*//////////////////////////////////////////////////////////////////////////
+    // Lifecycle
+    //////////////////////////////////////////////////////////////////////////*/
+
+    @Override
+    public void onCreate(@Nullable 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 LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+        return inflater.inflate(R.layout.fragment_instance_list, container, false);
+    }
+
+    @Override
+    public void onViewCreated(@NonNull View rootView, @Nullable Bundle savedInstanceState) {
+        super.onViewCreated(rootView, savedInstanceState);
+
+        initButton(rootView);
+
+        RecyclerView listInstances = rootView.findViewById(R.id.instances);
+        listInstances.setLayoutManager(new LinearLayoutManager(requireContext()));
+
+        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();
+        updateTitle();
+    }
+
+    @Override
+    public void onPause() {
+        super.onPause();
+        saveChanges();
+    }
+
+    @Override
+    public void onDestroy() {
+        super.onDestroy();
+        if (disposables != null) disposables.clear();
+        disposables = null;
+    }
+    /*//////////////////////////////////////////////////////////////////////////
+    // Menu
+    //////////////////////////////////////////////////////////////////////////*/
+
+    private final int MENU_ITEM_RESTORE_ID = 123456;
+
+    @Override
+    public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
+        super.onCreateOptionsMenu(menu, inflater);
+
+        final MenuItem restoreItem = menu.add(Menu.NONE, MENU_ITEM_RESTORE_ID, Menu.NONE, R.string.restore_defaults);
+        restoreItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
+
+        final int restoreIcon = ThemeHelper.resolveResourceIdFromAttr(requireContext(), R.attr.ic_restore_defaults);
+        restoreItem.setIcon(AppCompatResources.getDrawable(requireContext(), restoreIcon));
+    }
+
+    @Override
+    public boolean onOptionsItemSelected(MenuItem item) {
+        if (item.getItemId() == MENU_ITEM_RESTORE_ID) {
+            restoreDefaults();
+            return true;
+        }
+
+        return super.onOptionsItemSelected(item);
+    }
+
+    /*//////////////////////////////////////////////////////////////////////////
+    // Utils
+    //////////////////////////////////////////////////////////////////////////*/
+
+    private void updateInstanceList() {
+        instanceList.clear();
+        instanceList.addAll(PeertubeHelper.getInstanceList(requireContext()));
+    }
+
+    private void selectInstance(PeertubeInstance instance) {
+        selectedInstance = PeertubeHelper.selectInstance(instance, requireContext());
+        sharedPreferences.edit().putBoolean(Constants.KEY_MAIN_PAGE_CHANGE, true).apply();
+    }
+
+    private void updateTitle() {
+        if (getActivity() instanceof AppCompatActivity) {
+            ActionBar actionBar = ((AppCompatActivity) getActivity()).getSupportActionBar();
+            if (actionBar != null) actionBar.setTitle(R.string.peertube_instance_url_title);
+        }
+    }
+
+    private void saveChanges() {
+        JsonStringWriter jsonWriter = JsonWriter.string().object().array("instances");
+        for (PeertubeInstance instance : instanceList) {
+            jsonWriter.object();
+            jsonWriter.value("name", instance.getName());
+            jsonWriter.value("url", instance.getUrl());
+            jsonWriter.end();
+        }
+        String jsonToSave = jsonWriter.end().end().done();
+        sharedPreferences.edit().putString(savedInstanceListKey, jsonToSave).apply();
+    }
+
+    private void restoreDefaults() {
+        new AlertDialog.Builder(requireContext(), ThemeHelper.getDialogTheme(requireContext()))
+                .setTitle(R.string.restore_defaults)
+                .setMessage(R.string.restore_defaults_confirmation)
+                .setNegativeButton(R.string.cancel, null)
+                .setPositiveButton(R.string.yes, (dialog, which) -> {
+                    sharedPreferences.edit().remove(savedInstanceListKey).apply();
+                    selectInstance(PeertubeInstance.defaultInstance);
+                    updateInstanceList();
+                    instanceListAdapter.notifyDataSetChanged();
+                })
+                .show();
+    }
+
+    private void initButton(View rootView) {
+        final FloatingActionButton fab = rootView.findViewById(R.id.addInstanceButton);
+        fab.setOnClickListener(v -> {
+            showAddItemDialog(requireContext());
+        });
+    }
+
+    private void showAddItemDialog(Context c) {
+        final EditText urlET = new EditText(c);
+        urlET.setHint(R.string.peertube_instance_add_help);
+        AlertDialog dialog = new AlertDialog.Builder(c)
+                .setTitle(R.string.peertube_instance_add_title)
+                .setIcon(R.drawable.place_holder_peertube)
+                .setView(urlET)
+                .setNegativeButton(R.string.cancel, null)
+                .setPositiveButton(R.string.finish, (dialog1, which) -> {
+                    String url = urlET.getText().toString();
+                    addInstance(url);
+                })
+                .create();
+        dialog.show();
+    }
+
+    private void addInstance(String url) {
+        String cleanUrl = verifyUrl(url);
+        if(null == cleanUrl) return;
+        progressBar.setVisibility(View.VISIBLE);
+        Disposable disposable = Single.fromCallable(() -> {
+            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(), "failed to validate instance", Toast.LENGTH_SHORT).show();
+        });
+        disposables.add(disposable);
+    }
+
+    @Nullable
+    private String verifyUrl(String url){
+        // if protocol not present, add https
+        if(!url.startsWith("http")){
+            url = "https://" + url;
+        }
+        // remove trailing slash
+        url = url.replaceAll("/$", "");
+        // only allow https
+        if (!url.startsWith("https://")) {
+            Toast.makeText(getActivity(), "instance url should start with https://", Toast.LENGTH_SHORT).show();
+            return null;
+        }
+        // only allow if not already exists
+        for (PeertubeInstance instance : instanceList) {
+            if (instance.getUrl().equals(url)) {
+                Toast.makeText(getActivity(), "instance already exists", Toast.LENGTH_SHORT).show();
+                return null;
+            }
+        }
+        return url;
+    }
+
+    private void add(final PeertubeInstance instance) {
+        instanceList.add(instance);
+        instanceListAdapter.notifyDataSetChanged();
+    }
+
+    /*//////////////////////////////////////////////////////////////////////////
+    // List Handling
+    //////////////////////////////////////////////////////////////////////////*/
+
+    private class InstanceListAdapter extends RecyclerView.Adapter<InstanceListAdapter.TabViewHolder> {
+        private ItemTouchHelper itemTouchHelper;
+        private final LayoutInflater inflater;
+        private RadioButton lastChecked;
+
+        InstanceListAdapter(Context context, ItemTouchHelper itemTouchHelper) {
+            this.itemTouchHelper = itemTouchHelper;
+            this.inflater = LayoutInflater.from(context);
+        }
+
+        public void swapItems(int fromPosition, int toPosition) {
+            Collections.swap(instanceList, fromPosition, toPosition);
+            notifyItemMoved(fromPosition, toPosition);
+        }
+
+        @NonNull
+        @Override
+        public InstanceListAdapter.TabViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+            View view = inflater.inflate(R.layout.item_instance, parent, false);
+            return new InstanceListAdapter.TabViewHolder(view);
+        }
+
+        @Override
+        public void onBindViewHolder(@NonNull InstanceListAdapter.TabViewHolder holder, int position) {
+            holder.bind(position, holder);
+        }
+
+        @Override
+        public int getItemCount() {
+            return instanceList.size();
+        }
+
+        class TabViewHolder extends RecyclerView.ViewHolder {
+            private AppCompatImageView instanceIconView;
+            private TextView instanceNameView;
+            private TextView instanceUrlView;
+            private RadioButton instanceRB;
+            private ImageView handle;
+
+            TabViewHolder(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(int position, 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.place_holder_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;
+                };
+            }
+        }
+    }
+
+    private ItemTouchHelper.SimpleCallback getItemTouchCallback() {
+        return new ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP | ItemTouchHelper.DOWN,
+                ItemTouchHelper.START | ItemTouchHelper.END) {
+            @Override
+            public int interpolateOutOfBoundsScroll(RecyclerView recyclerView, int viewSize,
+                                                    int viewSizeOutOfBounds, int totalSize,
+                                                    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(RecyclerView recyclerView, RecyclerView.ViewHolder source,
+                                  RecyclerView.ViewHolder target) {
+                if (source.getItemViewType() != target.getItemViewType() ||
+                        instanceListAdapter == null) {
+                    return false;
+                }
+
+                final int sourceIndex = source.getAdapterPosition();
+                final int targetIndex = target.getAdapterPosition();
+                instanceListAdapter.swapItems(sourceIndex, targetIndex);
+                return true;
+            }
+
+            @Override
+            public boolean isLongPressDragEnabled() {
+                return false;
+            }
+
+            @Override
+            public boolean isItemViewSwipeEnabled() {
+                return true;
+            }
+
+            @Override
+            public void onSwiped(RecyclerView.ViewHolder viewHolder, int swipeDir) {
+                int position = viewHolder.getAdapterPosition();
+                // 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);
+                }
+            }
+        };
+    }
+}
diff --git a/app/src/main/java/org/schabi/newpipe/util/PeertubeHelper.java b/app/src/main/java/org/schabi/newpipe/util/PeertubeHelper.java
new file mode 100644
index 000000000..0d695e275
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/util/PeertubeHelper.java
@@ -0,0 +1,65 @@
+package org.schabi.newpipe.util;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.preference.PreferenceManager;
+
+import com.grack.nanojson.JsonArray;
+import com.grack.nanojson.JsonObject;
+import com.grack.nanojson.JsonParser;
+import com.grack.nanojson.JsonParserException;
+import com.grack.nanojson.JsonStringWriter;
+import com.grack.nanojson.JsonWriter;
+
+import org.schabi.newpipe.R;
+import org.schabi.newpipe.extractor.ServiceList;
+import org.schabi.newpipe.extractor.services.peertube.PeertubeInstance;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+public class PeertubeHelper {
+
+    public static List<PeertubeInstance> getInstanceList(Context context) {
+        SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
+        String savedInstanceListKey = context.getString(R.string.peertube_instance_list_key);
+        final String savedJson = sharedPreferences.getString(savedInstanceListKey, null);
+        if (null == savedJson) {
+            return Collections.singletonList(getCurrentInstance());
+        }
+
+        try {
+            JsonArray array = JsonParser.object().from(savedJson).getArray("instances");
+            List<PeertubeInstance> result = new ArrayList<>();
+            for (Object o : array) {
+                if (o instanceof JsonObject) {
+                    JsonObject instance = (JsonObject) o;
+                    String name = instance.getString("name");
+                    String url = instance.getString("url");
+                    result.add(new PeertubeInstance(url, name));
+                }
+            }
+            return result;
+        } catch (JsonParserException e) {
+            return Collections.singletonList(getCurrentInstance());
+        }
+
+    }
+
+    public static PeertubeInstance selectInstance(PeertubeInstance instance, Context context) {
+        SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
+        String selectedInstanceKey = context.getString(R.string.peertube_selected_instance_key);
+        JsonStringWriter jsonWriter = JsonWriter.string().object();
+        jsonWriter.value("name", instance.getName());
+        jsonWriter.value("url", instance.getUrl());
+        String jsonToSave = jsonWriter.end().done();
+        sharedPreferences.edit().putString(selectedInstanceKey, jsonToSave).apply();
+        ServiceList.PeerTube.setInstance(instance);
+        return instance;
+    }
+
+    public static PeertubeInstance getCurrentInstance(){
+        return ServiceList.PeerTube.getInstance();
+    }
+}
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 084ab5878..a25d4c9ec 100644
--- a/app/src/main/java/org/schabi/newpipe/util/ServiceHelper.java
+++ b/app/src/main/java/org/schabi/newpipe/util/ServiceHelper.java
@@ -3,9 +3,14 @@ package org.schabi.newpipe.util;
 import android.content.Context;
 import android.content.SharedPreferences;
 import android.preference.PreferenceManager;
+
 import androidx.annotation.DrawableRes;
 import androidx.annotation.StringRes;
 
+import com.grack.nanojson.JsonObject;
+import com.grack.nanojson.JsonParser;
+import com.grack.nanojson.JsonParserException;
+
 import org.schabi.newpipe.R;
 import org.schabi.newpipe.extractor.NewPipe;
 import org.schabi.newpipe.extractor.ServiceList;
@@ -137,11 +142,22 @@ public class ServiceHelper {
     }
 
     public static void initService(Context context, int serviceId) {
-        if(serviceId == ServiceList.PeerTube.getServiceId()){
+        if (serviceId == ServiceList.PeerTube.getServiceId()) {
             SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
-            String peerTubeInstanceUrl = sharedPreferences.getString(context.getString(R.string.peertube_instance_url_key), ServiceList.PeerTube.getBaseUrl());
-            String peerTubeInstanceName = sharedPreferences.getString(context.getString(R.string.peertube_instance_name_key), ServiceList.PeerTube.getServiceInfo().getName());
-            PeertubeInstance instance = new PeertubeInstance(peerTubeInstanceUrl, peerTubeInstanceName);
+            String json = sharedPreferences.getString(context.getString(R.string.peertube_selected_instance_key), null);
+            if (null == json) {
+                return;
+            }
+
+            JsonObject jsonObject = null;
+            try {
+                jsonObject = JsonParser.object().from(json);
+            } catch (JsonParserException e) {
+                return;
+            }
+            String name = jsonObject.getString("name");
+            String url = jsonObject.getString("url");
+            PeertubeInstance instance = new PeertubeInstance(url, name);
             ServiceList.PeerTube.setInstance(instance);
         }
     }
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 f476cf66b..530c8cefe 100644
--- a/app/src/main/java/org/schabi/newpipe/util/ThemeHelper.java
+++ b/app/src/main/java/org/schabi/newpipe/util/ThemeHelper.java
@@ -137,12 +137,7 @@ public class ThemeHelper {
         else if (selectedTheme.equals(blackTheme)) themeName = "BlackTheme";
         else if (selectedTheme.equals(darkTheme)) themeName = "DarkTheme";
 
-        if(serviceId == ServiceList.PeerTube.getServiceId()){
-            //service name for peertube depends on the instance
-            themeName += ".PeerTube";
-        }else{
-            themeName += "." + service.getServiceInfo().getName();
-        }
+        themeName += "." + service.getServiceInfo().getName();
 
         int resourceId = context
                 .getResources()
diff --git a/app/src/main/res/layout/fragment_instance_list.xml b/app/src/main/res/layout/fragment_instance_list.xml
new file mode 100644
index 000000000..970b67c26
--- /dev/null
+++ b/app/src/main/res/layout/fragment_instance_list.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical">
+
+    <TextView
+        android:id="@+id/instanceHelpTV"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_margin="15dp"
+        android:autoLink="web"
+        android:text="@string/peertube_instance_url_help"/>
+
+    <androidx.recyclerview.widget.RecyclerView
+        android:id="@+id/instances"
+        android:layout_below="@id/instanceHelpTV"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        tools:listitem="@layout/item_instance" />
+
+    <!-- LOADING INDICATOR-->
+    <ProgressBar
+        android:id="@+id/loading_progress_bar"
+        style="@style/Widget.AppCompat.ProgressBar"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_centerInParent="true"
+        android:indeterminate="true"
+        android:visibility="gone"
+        tools:visibility="visible" />
+
+    <com.google.android.material.floatingactionbutton.FloatingActionButton
+        android:id="@+id/addInstanceButton"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_alignParentEnd="true"
+        android:layout_alignParentRight="true"
+        android:layout_alignParentBottom="true"
+        android:layout_marginEnd="16dp"
+        android:layout_marginRight="16dp"
+        android:layout_marginBottom="16dp"
+        android:clickable="true"
+        android:focusable="true"
+        app:backgroundTint="?attr/colorPrimary"
+        app:fabSize="auto"
+        app:srcCompat="?attr/ic_add" />
+
+</RelativeLayout>
\ No newline at end of file
diff --git a/app/src/main/res/layout/instance_spinner_item.xml b/app/src/main/res/layout/instance_spinner_item.xml
new file mode 100644
index 000000000..1edac71af
--- /dev/null
+++ b/app/src/main/res/layout/instance_spinner_item.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<TextView xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@android:id/text1"
+    android:layout_width="wrap_content"
+    android:layout_height="match_parent"
+    android:maxLength="0" />
diff --git a/app/src/main/res/layout/instance_spinner_layout.xml b/app/src/main/res/layout/instance_spinner_layout.xml
new file mode 100644
index 000000000..63e910d96
--- /dev/null
+++ b/app/src/main/res/layout/instance_spinner_layout.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Spinner xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:id="@+id/spinner"
+    tools:listitem="@layout/instance_spinner_item"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:gravity="end"
+    android:prompt="@string/choose_instance_prompt" />
diff --git a/app/src/main/res/layout/item_instance.xml b/app/src/main/res/layout/item_instance.xml
new file mode 100644
index 000000000..b0e4e25bd
--- /dev/null
+++ b/app/src/main/res/layout/item_instance.xml
@@ -0,0 +1,83 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.cardview.widget.CardView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:id="@+id/layoutCard"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:layout_marginBottom="3dp"
+    android:layout_marginLeft="5dp"
+    android:layout_marginRight="5dp"
+    android:layout_marginTop="3dp"
+    android:minHeight="?listPreferredItemHeightSmall"
+    android:orientation="horizontal"
+    app:cardCornerRadius="5dp"
+    app:cardElevation="4dp">
+
+    <RelativeLayout
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:layout_gravity="center_vertical">
+
+        <androidx.appcompat.widget.AppCompatImageView
+            android:id="@+id/instanceIcon"
+            android:layout_width="24dp"
+            android:layout_height="24dp"
+            android:layout_centerVertical="true"
+            android:layout_alignParentLeft="true"
+            android:layout_marginLeft="10dp"
+            tools:ignore="ContentDescription,RtlHardcoded"
+            tools:src="@drawable/place_holder_peertube"/>
+
+        <TextView
+            android:id="@+id/instanceName"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginLeft="10dp"
+            android:layout_marginTop="6dp"
+            android:layout_toRightOf="@+id/instanceIcon"
+            android:layout_toLeftOf="@id/selectInstanceRB"
+            android:singleLine="true"
+            android:ellipsize="marquee"
+            android:textAppearance="?textAppearanceListItem"
+            tools:ignore="RtlHardcoded"
+            tools:text="Framatube"/>
+
+        <TextView
+            android:id="@+id/instanceUrl"
+            android:autoLink="web"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginBottom="6dp"
+            android:layout_marginLeft="10dp"
+            android:layout_toRightOf="@id/instanceIcon"
+            android:layout_toLeftOf="@id/selectInstanceRB"
+            android:layout_below="@id/instanceName"
+            android:singleLine="true"
+            android:ellipsize="marquee"
+            android:textAppearance="?textAppearanceListItemSecondary"
+            tools:ignore="RtlHardcoded"
+            tools:text="https://framatube.org"/>
+
+        <RadioButton
+            android:id="@+id/selectInstanceRB"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_toLeftOf="@id/handle"
+            android:layout_centerVertical="true"/>
+
+        <androidx.appcompat.widget.AppCompatImageView
+            android:id="@+id/handle"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_alignParentRight="true"
+            android:layout_centerVertical="true"
+            android:paddingBottom="12dp"
+            android:paddingLeft="16dp"
+            android:paddingRight="10dp"
+            android:paddingTop="12dp"
+            android:src="?attr/drag_handle"
+            tools:ignore="ContentDescription,RtlHardcoded"/>
+    </RelativeLayout>
+</androidx.cardview.widget.CardView>
\ No newline at end of file
diff --git a/app/src/main/res/values/settings_keys.xml b/app/src/main/res/values/settings_keys.xml
index dcf39b488..2249d1ec0 100644
--- a/app/src/main/res/values/settings_keys.xml
+++ b/app/src/main/res/values/settings_keys.xml
@@ -144,8 +144,9 @@
     <string name="default_language_value">en</string>
     <string name="default_country_value">GB</string>
     <string name="content_language_key" translatable="false">content_language</string>
-    <string name="peertube_instance_url_key" translatable="false">peertube_instance_url</string>
-    <string name="peertube_instance_name_key" translatable="false">peertube_instance_name</string>
+    <string name="peertube_instance_setup_key" translatable="false">peertube_instance_setup</string>
+    <string name="peertube_selected_instance_key" translatable="false">peertube_selected_instance</string>
+    <string name="peertube_instance_list_key" translatable="false">peertube_instance_list</string>
     <string name="content_country_key" translatable="false">content_country</string>
     <string name="show_age_restricted_content" translatable="false">show_age_restricted_content</string>
     <string name="use_tor_key" translatable="false">use_tor</string>
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 52b56a7b8..c652b7f65 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -109,8 +109,11 @@
     <string name="default_content_country_title">Default content country</string>
     <string name="service_title">Service</string>
     <string name="content_language_title">Default content language</string>
-    <string name="peertube_instance_url_title">PeerTube instance</string>
+    <string name="peertube_instance_url_title">PeerTube instances</string>
+    <string name="peertube_instance_url_summary">Set your favorite peertube instances</string>
     <string name="peertube_instance_url_help">Find the instance that best suits you on https://instances.joinpeertube.org</string>
+    <string name="peertube_instance_add_title">Add instance</string>
+    <string name="peertube_instance_add_help">enter instance url</string>
     <string name="settings_category_player_title">Player</string>
     <string name="settings_category_player_behavior_title">Behavior</string>
     <string name="settings_category_video_audio_title">Video &amp; audio</string>
@@ -578,4 +581,6 @@
     <string name="downloads_storage_ask_summary_kitkat">You will be asked where to save each download.\nChoose SAF if you want to download to an external SD card</string>
     <string name="downloads_storage_use_saf_title">Use SAF</string>
     <string name="downloads_storage_use_saf_summary">The Storage Access Framework allows downloads to an external SD card.\nNote: some devices are not compatible</string>
+    <string name="choose_instance_prompt">Choose an instance</string>
+
 </resources>
diff --git a/app/src/main/res/xml/content_settings.xml b/app/src/main/res/xml/content_settings.xml
index 6d2329310..0d579ba35 100644
--- a/app/src/main/res/xml/content_settings.xml
+++ b/app/src/main/res/xml/content_settings.xml
@@ -12,12 +12,12 @@
         android:summary="%s"
         android:title="@string/content_language_title"/>
 
-    <EditTextPreference
+    <PreferenceScreen
         app:iconSpaceReserved="false"
-        android:dialogMessage="@string/peertube_instance_url_help"
-        android:key="@string/peertube_instance_url_key"
+        android:fragment="org.schabi.newpipe.settings.PeertubeInstanceListFragment"
+        android:key="@string/peertube_instance_setup_key"
         android:title="@string/peertube_instance_url_title"
-        android:inputType="textUri" />
+        android:summary="@string/peertube_instance_url_summary"/>
 
     <ListPreference
         app:iconSpaceReserved="false"