diff --git a/app/build.gradle b/app/build.gradle index 7e4707f99..3b0b8fe6c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -62,7 +62,8 @@ dependencies { exclude module: 'support-annotations' }) - implementation 'com.github.TeamNewPipe:NewPipeExtractor:b6d3252' + implementation 'com.github.TeamNewPipe:NewPipeExtractor:43b54cc' + testImplementation 'junit:junit:4.12' testImplementation 'org.mockito:mockito-core:2.23.0' diff --git a/app/src/main/java/org/schabi/newpipe/App.java b/app/src/main/java/org/schabi/newpipe/App.java index 8698b3c93..7f050e6c7 100644 --- a/app/src/main/java/org/schabi/newpipe/App.java +++ b/app/src/main/java/org/schabi/newpipe/App.java @@ -6,9 +6,9 @@ import android.app.NotificationChannel; import android.app.NotificationManager; import android.content.Context; import android.os.Build; +import android.util.Log; import androidx.annotation.Nullable; -import android.util.Log; import com.nostra13.universalimageloader.cache.memory.impl.LRULimitedMemoryCache; import com.nostra13.universalimageloader.core.ImageLoader; @@ -29,6 +29,7 @@ import org.schabi.newpipe.report.UserAction; import org.schabi.newpipe.settings.SettingsActivity; import org.schabi.newpipe.util.ExtractorHelper; import org.schabi.newpipe.util.Localization; +import org.schabi.newpipe.util.ServiceHelper; import org.schabi.newpipe.util.StateSaver; import java.io.IOException; @@ -103,6 +104,8 @@ public class App extends Application { StateSaver.init(this); initNotificationChannel(); + ServiceHelper.initServices(this); + // Initialize image loader ImageLoader.getInstance().init(getImageLoaderConfigurations(10, 50)); diff --git a/app/src/main/java/org/schabi/newpipe/MainActivity.java b/app/src/main/java/org/schabi/newpipe/MainActivity.java index c24d77d03..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 instances = PeertubeHelper.getInstanceList(this); + List 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 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); @@ -367,6 +422,7 @@ public class MainActivity extends AppCompatActivity { String selectedServiceName = NewPipe.getService( ServiceHelper.getSelectedServiceId(this)).getServiceInfo().getName(); headerServiceView.setText(selectedServiceName); + headerServiceView.post(() -> headerServiceView.setSelected(true)); toggleServiceButton.setContentDescription( getString(R.string.drawer_header_description) + selectedServiceName); } catch (Exception e) { diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java index 95aef4764..c20ff0fc2 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java @@ -29,6 +29,7 @@ import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.ListExtractor; import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.channel.ChannelInfo; +import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException; import org.schabi.newpipe.extractor.exceptions.ExtractionException; import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.fragments.list.BaseListInfoFragment; @@ -98,7 +99,7 @@ public class ChannelFragment extends BaseListInfoFragment { @Override public void setUserVisibleHint(boolean isVisibleToUser) { super.setUserVisibleHint(isVisibleToUser); - if(activity != null + if (activity != null && useAsFrontPage && isVisibleToUser) { setTitle(currentInfo != null ? currentInfo.getName() : name); @@ -152,7 +153,7 @@ public class ChannelFragment extends BaseListInfoFragment { public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { super.onCreateOptionsMenu(menu, inflater); ActionBar supportActionBar = activity.getSupportActionBar(); - if(useAsFrontPage && supportActionBar != null) { + if (useAsFrontPage && supportActionBar != null) { supportActionBar.setDisplayHomeAsUpEnabled(false); } else { inflater.inflate(R.menu.menu_channel, menu); @@ -165,7 +166,7 @@ public class ChannelFragment extends BaseListInfoFragment { private void openRssFeed() { final ChannelInfo info = currentInfo; - if(info != null) { + if (info != null) { Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(info.getFeedUrl())); startActivity(intent); } @@ -178,10 +179,14 @@ public class ChannelFragment extends BaseListInfoFragment { openRssFeed(); break; case R.id.menu_item_openInBrowser: - ShareUtils.openUrlInBrowser(this.getContext(), currentInfo.getOriginalUrl()); + if (currentInfo != null) { + ShareUtils.openUrlInBrowser(this.getContext(), currentInfo.getOriginalUrl()); + } break; case R.id.menu_item_share: - ShareUtils.shareUrl(this.getContext(), name, currentInfo.getOriginalUrl()); + if (currentInfo != null) { + ShareUtils.shareUrl(this.getContext(), name, currentInfo.getOriginalUrl()); + } break; default: return super.onOptionsItemSelected(item); @@ -218,7 +223,7 @@ public class ChannelFragment extends BaseListInfoFragment { .debounce(100, TimeUnit.MILLISECONDS) .observeOn(AndroidSchedulers.mainThread()) .subscribe((List subscriptionEntities) -> - updateSubscribeButton(!subscriptionEntities.isEmpty()) + updateSubscribeButton(!subscriptionEntities.isEmpty()) , onError)); } @@ -359,9 +364,9 @@ public class ChannelFragment extends BaseListInfoFragment { headerRootLayout.setVisibility(View.VISIBLE); imageLoader.displayImage(result.getBannerUrl(), headerChannelBanner, - ImageDisplayConstants.DISPLAY_BANNER_OPTIONS); + ImageDisplayConstants.DISPLAY_BANNER_OPTIONS); imageLoader.displayImage(result.getAvatarUrl(), headerAvatarView, - ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS); + ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS); headerSubscribersTextView.setVisibility(View.VISIBLE); if (result.getSubscriberCount() >= 0) { @@ -397,8 +402,8 @@ public class ChannelFragment extends BaseListInfoFragment { private PlayQueue getPlayQueue(final int index) { final List streamItems = new ArrayList<>(); - for(InfoItem i : infoListAdapter.getItemsList()) { - if(i instanceof StreamInfoItem) { + for (InfoItem i : infoListAdapter.getItemsList()) { + if (i instanceof StreamInfoItem) { streamItems.add((StreamInfoItem) i); } } @@ -432,12 +437,16 @@ public class ChannelFragment extends BaseListInfoFragment { protected boolean onError(Throwable exception) { if (super.onError(exception)) return true; - int errorId = exception instanceof ExtractionException ? R.string.parsing_error : R.string.general_error; - onUnrecoverableError(exception, - UserAction.REQUESTED_CHANNEL, - NewPipe.getNameOfService(serviceId), - url, - errorId); + if (exception instanceof ContentNotAvailableException) { + showError(getString(R.string.content_not_available), false); + } else { + int errorId = exception instanceof ExtractionException ? R.string.parsing_error : R.string.general_error; + onUnrecoverableError(exception, + UserAction.REQUESTED_CHANNEL, + NewPipe.getNameOfService(serviceId), + url, + errorId); + } return true; } 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..d8c36e5cb --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/settings/PeertubeInstanceListFragment.java @@ -0,0 +1,417 @@ +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.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 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 = cleanUrl(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(), R.string.peertube_instance_add_fail, Toast.LENGTH_SHORT).show(); + }); + disposables.add(disposable); + } + + @Nullable + private String cleanUrl(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(), R.string.peertube_instance_add_https_only, Toast.LENGTH_SHORT).show(); + return null; + } + // only allow if not already exists + for (PeertubeInstance instance : instanceList) { + if (instance.getUrl().equals(url)) { + Toast.makeText(getActivity(), R.string.peertube_instance_add_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 { + 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/KioskTranslator.java b/app/src/main/java/org/schabi/newpipe/util/KioskTranslator.java index a04e1145f..18c95e394 100644 --- a/app/src/main/java/org/schabi/newpipe/util/KioskTranslator.java +++ b/app/src/main/java/org/schabi/newpipe/util/KioskTranslator.java @@ -31,6 +31,12 @@ public class KioskTranslator { return c.getString(R.string.top_50); case "New & hot": return c.getString(R.string.new_and_hot); + case "Local": + return c.getString(R.string.local); + case "Recently added": + return c.getString(R.string.recently_added); + case "Most liked": + return c.getString(R.string.most_liked); case "conferences": return c.getString(R.string.conferences); default: @@ -46,6 +52,12 @@ public class KioskTranslator { return ThemeHelper.resolveResourceIdFromAttr(c, R.attr.ic_hot); case "New & hot": return ThemeHelper.resolveResourceIdFromAttr(c, R.attr.ic_hot); + case "Local": + return ThemeHelper.resolveResourceIdFromAttr(c, R.attr.ic_kiosk_local); + case "Recently added": + return ThemeHelper.resolveResourceIdFromAttr(c, R.attr.ic_kiosk_recent); + case "Most liked": + return ThemeHelper.resolveResourceIdFromAttr(c, R.attr.thumbs_up); case "conferences": return ThemeHelper.resolveResourceIdFromAttr(c, R.attr.ic_hot); default: 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 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 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 27e2f8422..8929cc654 100644 --- a/app/src/main/java/org/schabi/newpipe/util/ServiceHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/ServiceHelper.java @@ -1,15 +1,22 @@ 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; import org.schabi.newpipe.extractor.StreamingService; import org.schabi.newpipe.extractor.exceptions.ExtractionException; +import org.schabi.newpipe.extractor.services.peertube.PeertubeInstance; import java.util.concurrent.TimeUnit; @@ -27,13 +34,15 @@ public class ServiceHelper { return R.drawable.place_holder_cloud; case 2: return R.drawable.place_holder_gadse; + case 3: + return R.drawable.place_holder_peertube; default: return R.drawable.place_holder_circle; } } public static String getTranslatedFilterString(String filter, Context c) { - switch(filter) { + switch (filter) { case "all": return c.getString(R.string.all); case "videos": return c.getString(R.string.videos); case "channels": return c.getString(R.string.channels); @@ -126,9 +135,36 @@ public class ServiceHelper { } public static boolean isBeta(final StreamingService s) { - switch(s.getServiceInfo().getName()) { + switch (s.getServiceInfo().getName()) { case "YouTube": return false; default: return true; } } + + public static void initService(Context context, int serviceId) { + if (serviceId == ServiceList.PeerTube.getServiceId()) { + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); + 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); + } + } + + public static void initServices(Context context) { + for (StreamingService s : ServiceList.all()) { + initService(context, s.getServiceId()); + } + } } diff --git a/app/src/main/res/drawable-hdpi/ic_kiosk_local_black_24dp.png b/app/src/main/res/drawable-hdpi/ic_kiosk_local_black_24dp.png new file mode 100755 index 000000000..a9e2993eb Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_kiosk_local_black_24dp.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_kiosk_local_white_24dp.png b/app/src/main/res/drawable-hdpi/ic_kiosk_local_white_24dp.png new file mode 100755 index 000000000..a9af000b4 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_kiosk_local_white_24dp.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_kiosk_recent_black_24dp.png b/app/src/main/res/drawable-hdpi/ic_kiosk_recent_black_24dp.png new file mode 100755 index 000000000..13813ff82 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_kiosk_recent_black_24dp.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_kiosk_recent_white_24dp.png b/app/src/main/res/drawable-hdpi/ic_kiosk_recent_white_24dp.png new file mode 100755 index 000000000..9054e0042 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_kiosk_recent_white_24dp.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_kiosk_local_black_24dp.png b/app/src/main/res/drawable-mdpi/ic_kiosk_local_black_24dp.png new file mode 100755 index 000000000..1eba63792 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_kiosk_local_black_24dp.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_kiosk_local_white_24dp.png b/app/src/main/res/drawable-mdpi/ic_kiosk_local_white_24dp.png new file mode 100755 index 000000000..23d8145f5 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_kiosk_local_white_24dp.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_kiosk_recent_black_24dp.png b/app/src/main/res/drawable-mdpi/ic_kiosk_recent_black_24dp.png new file mode 100755 index 000000000..adc36b227 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_kiosk_recent_black_24dp.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_kiosk_recent_white_24dp.png b/app/src/main/res/drawable-mdpi/ic_kiosk_recent_white_24dp.png new file mode 100755 index 000000000..c19bfe964 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_kiosk_recent_white_24dp.png differ diff --git a/app/src/main/res/drawable-nodpi/place_holder_peertube.png b/app/src/main/res/drawable-nodpi/place_holder_peertube.png new file mode 100644 index 000000000..68850054d Binary files /dev/null and b/app/src/main/res/drawable-nodpi/place_holder_peertube.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_kiosk_local_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_kiosk_local_black_24dp.png new file mode 100755 index 000000000..e20865ab0 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_kiosk_local_black_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_kiosk_local_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_kiosk_local_white_24dp.png new file mode 100755 index 000000000..2d3474832 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_kiosk_local_white_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_kiosk_recent_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_kiosk_recent_black_24dp.png new file mode 100755 index 000000000..54e815980 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_kiosk_recent_black_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_kiosk_recent_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_kiosk_recent_white_24dp.png new file mode 100755 index 000000000..3141a790d Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_kiosk_recent_white_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_kiosk_local_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_kiosk_local_black_24dp.png new file mode 100755 index 000000000..bcbeb199c Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_kiosk_local_black_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_kiosk_local_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_kiosk_local_white_24dp.png new file mode 100755 index 000000000..6b27fb23c Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_kiosk_local_white_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_kiosk_recent_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_kiosk_recent_black_24dp.png new file mode 100755 index 000000000..92fc748ec Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_kiosk_recent_black_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_kiosk_recent_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_kiosk_recent_white_24dp.png new file mode 100755 index 000000000..5b0aa6ae2 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_kiosk_recent_white_24dp.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_kiosk_local_black_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_kiosk_local_black_24dp.png new file mode 100755 index 000000000..e208010a2 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_kiosk_local_black_24dp.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_kiosk_local_white_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_kiosk_local_white_24dp.png new file mode 100755 index 000000000..b04fd7a88 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_kiosk_local_white_24dp.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_kiosk_recent_black_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_kiosk_recent_black_24dp.png new file mode 100755 index 000000000..152259fab Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_kiosk_recent_black_24dp.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_kiosk_recent_white_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_kiosk_recent_white_24dp.png new file mode 100755 index 000000000..1aac3b986 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_kiosk_recent_white_24dp.png differ diff --git a/app/src/main/res/layout-v21/drawer_header.xml b/app/src/main/res/layout-v21/drawer_header.xml index 22e81883d..9ed9f833a 100644 --- a/app/src/main/res/layout-v21/drawer_header.xml +++ b/app/src/main/res/layout-v21/drawer_header.xml @@ -47,15 +47,22 @@ + android:textStyle="italic" + android:ellipsize="marquee" + android:fadingEdge="horizontal" + android:marqueeRepeatLimit="marquee_forever" + android:scrollHorizontally="true" + android:singleLine="true" /> + android:textStyle="italic" + android:ellipsize="marquee" + android:fadingEdge="horizontal" + android:marqueeRepeatLimit="marquee_forever" + android:scrollHorizontally="true" + android:singleLine="true" /> + app:headerLayout="@layout/drawer_header" + android:theme="@style/NavViewTextStyle"/> + + + + + + + + + + + + \ 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 @@ + + 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 @@ + + 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 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-v21/styles_services.xml b/app/src/main/res/values-v21/styles_services.xml index 6c118bc09..176bc1f51 100644 --- a/app/src/main/res/values-v21/styles_services.xml +++ b/app/src/main/res/values-v21/styles_services.xml @@ -31,6 +31,25 @@ @color/dark_soundcloud_accent_color + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index 30bf8f43f..c64ed1256 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -29,6 +29,8 @@ + + diff --git a/app/src/main/res/values/colors_services.xml b/app/src/main/res/values/colors_services.xml index ea90cb083..0126ee9ae 100644 --- a/app/src/main/res/values/colors_services.xml +++ b/app/src/main/res/values/colors_services.xml @@ -22,6 +22,17 @@ #FFFFFF #ff9100 + + #ff6f00 + #c43e00 + #000000 + #ff833a + + #ff6f00 + #c43e00 + #FFFFFF + #ff833a + #9e9e9e #616161 diff --git a/app/src/main/res/values/settings_keys.xml b/app/src/main/res/values/settings_keys.xml index 783ab7b5e..8dcc2ce31 100644 --- a/app/src/main/res/values/settings_keys.xml +++ b/app/src/main/res/values/settings_keys.xml @@ -145,6 +145,9 @@ en GB content_language + peertube_instance_setup + peertube_selected_instance + peertube_instance_list content_country show_age_restricted_content use_tor diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a0e84974c..df7800cf5 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -109,6 +109,14 @@ Default content country Service Default content language + PeerTube instances + Set your favorite peertube instances + Find the instances that best suit you on https://joinpeertube.org/instances#instances-list + Add instance + Enter instance url + Failed to validate instance + Only https urls are supported + Instance already exists Player Behavior Video & audio @@ -400,6 +408,9 @@ Trending Top 50 New & hot + Local + Recently added + Most liked Conferences %1$s/%2$s @@ -577,4 +588,6 @@ You will be asked where to save each download.\nChoose SAF if you want to download to an external SD card Use SAF The Storage Access Framework allows downloads to an external SD card.\nNote: some devices are not compatible + Choose an instance + diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 360a00f93..ba3fe78d5 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -44,6 +44,8 @@ @drawable/ic_pause_black_24dp @drawable/ic_settings_black_24dp @drawable/ic_whatshot_black_24dp + @drawable/ic_kiosk_local_black_24dp + @drawable/ic_kiosk_recent_black_24dp @drawable/ic_channel_black_24dp @drawable/ic_bookmark_black_24dp @drawable/ic_playlist_add_black_24dp @@ -108,6 +110,8 @@ @drawable/ic_play_arrow_white_24dp @drawable/ic_settings_white_24dp @drawable/ic_whatshot_white_24dp + @drawable/ic_kiosk_local_white_24dp + @drawable/ic_kiosk_recent_white_24dp @drawable/ic_channel_white_24dp @drawable/ic_bookmark_white_24dp @drawable/ic_playlist_add_white_24dp @@ -233,4 +237,8 @@ true @null + + diff --git a/app/src/main/res/values/styles_services.xml b/app/src/main/res/values/styles_services.xml index d6ab239e4..28490d7c6 100644 --- a/app/src/main/res/values/styles_services.xml +++ b/app/src/main/res/values/styles_services.xml @@ -32,6 +32,25 @@ @drawable/progress_soundcloud_horizontal_dark + + + + + + +