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 & 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"