diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 54415858e..f6708fa83 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -111,6 +111,7 @@ jobs: path: app/build/reports/androidTests/connected/** sonar: + if: ${{ false }} # the key has expired and needs to be regenerated by the sonar admins runs-on: ubuntu-latest permissions: diff --git a/README.md b/README.md index bf1317f17..c19144064 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,30 @@ -

We are planning to rewrite large chunks of the codebase, to bring about a new, modern and stable NewPipe!

-

Please do not open pull requests for new features now, only bugfix PRs will be accepted.

+

We are rewriting large chunks of the codebase, to bring about a modern and stable NewPipe! You can download nightly builds here.

+

Please work on the refactor branch if you want to contribute new features. The current codebase is in maintenance mode and will only receive bugfixes.

NewPipe

A libre lightweight streaming front-end for Android.

-

Get it on F-Droid

+

Get it on F-Droid

- + + + + + + + - + +

+ +

+

ScreenshotsSupported ServicesDescriptionFeaturesInstallation and updatesContributionDonateLicense

WebsiteBlogFAQPress

diff --git a/app/src/main/java/org/schabi/newpipe/MainActivity.java b/app/src/main/java/org/schabi/newpipe/MainActivity.java index 95b1f4164..bf23d3d70 100644 --- a/app/src/main/java/org/schabi/newpipe/MainActivity.java +++ b/app/src/main/java/org/schabi/newpipe/MainActivity.java @@ -122,7 +122,10 @@ public class MainActivity extends AppCompatActivity { private static final int ITEM_ID_ABOUT = 2; private static final int ORDER = 0; + public static final String KEY_IS_IN_BACKGROUND = "is_in_background"; + private SharedPreferences sharedPreferences; + private SharedPreferences.Editor sharedPrefEditor; /*////////////////////////////////////////////////////////////////////////// // Activity's LifeCycle //////////////////////////////////////////////////////////////////////////*/ @@ -152,6 +155,8 @@ public class MainActivity extends AppCompatActivity { assureCorrectAppLanguage(this); super.onCreate(savedInstanceState); + sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); + sharedPrefEditor = sharedPreferences.edit(); mainBinding = ActivityMainBinding.inflate(getLayoutInflater()); drawerLayoutBinding = mainBinding.drawerLayout; @@ -195,16 +200,29 @@ public class MainActivity extends AppCompatActivity { super.onPostCreate(savedInstanceState); final App app = App.getInstance(); - final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(app); - if (prefs.getBoolean(app.getString(R.string.update_app_key), false) - && prefs.getBoolean(app.getString(R.string.update_check_consent_key), false)) { + if (sharedPreferences.getBoolean(app.getString(R.string.update_app_key), false) + && sharedPreferences + .getBoolean(app.getString(R.string.update_check_consent_key), false)) { // Start the worker which is checking all conditions // and eventually searching for a new version. NewVersionWorker.enqueueNewVersionCheckingWork(app, false); } } + @Override + protected void onStart() { + super.onStart(); + sharedPrefEditor.putBoolean(KEY_IS_IN_BACKGROUND, false).apply(); + Log.d(TAG, "App moved to foreground"); + } + + @Override + protected void onStop() { + super.onStop(); + sharedPrefEditor.putBoolean(KEY_IS_IN_BACKGROUND, true).apply(); + Log.d(TAG, "App moved to background"); + } private void setupDrawer() throws ExtractionException { addDrawerMenuForCurrentService(); @@ -504,13 +522,11 @@ public class MainActivity extends AppCompatActivity { ErrorUtil.showUiErrorSnackbar(this, "Setting up service toggle", e); } - final SharedPreferences sharedPreferences = - PreferenceManager.getDefaultSharedPreferences(this); if (sharedPreferences.getBoolean(Constants.KEY_THEME_CHANGE, false)) { if (DEBUG) { Log.d(TAG, "Theme has changed, recreating activity..."); } - sharedPreferences.edit().putBoolean(Constants.KEY_THEME_CHANGE, false).apply(); + sharedPrefEditor.putBoolean(Constants.KEY_THEME_CHANGE, false).apply(); ActivityCompat.recreate(this); } @@ -518,7 +534,7 @@ public class MainActivity extends AppCompatActivity { if (DEBUG) { Log.d(TAG, "main page has changed, recreating main fragment..."); } - sharedPreferences.edit().putBoolean(Constants.KEY_MAIN_PAGE_CHANGE, false).apply(); + sharedPrefEditor.putBoolean(Constants.KEY_MAIN_PAGE_CHANGE, false).apply(); NavigationHelper.openMainActivity(this); } diff --git a/app/src/main/java/org/schabi/newpipe/error/ErrorUtil.kt b/app/src/main/java/org/schabi/newpipe/error/ErrorUtil.kt index dcbc11413..93dd8e522 100644 --- a/app/src/main/java/org/schabi/newpipe/error/ErrorUtil.kt +++ b/app/src/main/java/org/schabi/newpipe/error/ErrorUtil.kt @@ -11,7 +11,9 @@ import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import androidx.core.app.PendingIntentCompat import androidx.fragment.app.Fragment +import androidx.preference.PreferenceManager import com.google.android.material.snackbar.Snackbar +import org.schabi.newpipe.MainActivity import org.schabi.newpipe.R /** @@ -35,12 +37,20 @@ class ErrorUtil { * activity (since the workflow would be interrupted anyway in that case). So never use this * for background services. * + * If the crashed occurred while the app was in the background open a notification instead + * * @param context the context to use to start the new activity * @param errorInfo the error info to be reported */ @JvmStatic fun openActivity(context: Context, errorInfo: ErrorInfo) { - context.startActivity(getErrorActivityIntent(context, errorInfo)) + if (PreferenceManager.getDefaultSharedPreferences(context) + .getBoolean(MainActivity.KEY_IS_IN_BACKGROUND, true) + ) { + createNotification(context, errorInfo) + } else { + context.startActivity(getErrorActivityIntent(context, errorInfo)) + } } /** diff --git a/app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.kt b/app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.kt index f976f44aa..038f2bed1 100644 --- a/app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.kt +++ b/app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.kt @@ -274,7 +274,12 @@ class FeedFragment : BaseStateFragment() { @Deprecated("Deprecated in Java") override fun onDestroyOptionsMenu() { super.onDestroyOptionsMenu() - activity?.supportActionBar?.subtitle = null + if ( + (groupName != "") && + (activity?.supportActionBar?.subtitle == groupName) + ) { + activity?.supportActionBar?.subtitle = null + } } override fun onDestroy() { @@ -286,7 +291,13 @@ class FeedFragment : BaseStateFragment() { } super.onDestroy() - activity?.supportActionBar?.subtitle = null + + if ( + (groupName != "") && + (activity?.supportActionBar?.subtitle == groupName) + ) { + activity?.supportActionBar?.subtitle = null + } } override fun onDestroyView() { diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionFragment.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionFragment.kt index 91ff7cd27..8c5d05394 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionFragment.kt +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionFragment.kt @@ -12,6 +12,7 @@ import android.view.MenuItem import android.view.SubMenu import android.view.View import android.view.ViewGroup +import android.webkit.MimeTypeMap import android.widget.Toast import androidx.activity.result.ActivityResult import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult @@ -456,6 +457,7 @@ class SubscriptionFragment : BaseStateFragment() { } companion object { - const val JSON_MIME_TYPE = "application/json" + val JSON_MIME_TYPE = MimeTypeMap.getSingleton() + .getMimeTypeFromExtension("json") ?: "application/octet-stream" } } diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHolder.java b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHolder.java index 5452068d9..ba8a5e0ff 100644 --- a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHolder.java +++ b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHolder.java @@ -167,6 +167,11 @@ public final class PlayerHolder { private boolean playAfterConnect = false; + /** + * @param playAfterConnection Sets the value of `playAfterConnect` to pass to the {@link + * PlayerServiceExtendedEventListener#onPlayerConnected(Player, boolean)} the next time it + * is called. The value of `playAfterConnect` will be reset to false after that. + */ public void doPlayAfterConnect(final boolean playAfterConnection) { this.playAfterConnect = playAfterConnection; } @@ -371,6 +376,8 @@ public final class PlayerHolder { listener.onPlayerDisconnected(); } else { listener.onPlayerConnected(player, serviceConnection.playAfterConnect); + // reset the value of playAfterConnect: if it was true before, it is now "consumed" + serviceConnection.playAfterConnect = false; player.setFragmentListener(internalListener); } } diff --git a/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserImpl.kt b/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserImpl.kt index 3108da80f..f15d7ab08 100644 --- a/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserImpl.kt +++ b/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserImpl.kt @@ -8,6 +8,7 @@ import android.support.v4.media.MediaBrowserCompat import android.support.v4.media.MediaDescriptionCompat import android.util.Log import androidx.annotation.DrawableRes +import androidx.core.net.toUri import androidx.media.MediaBrowserServiceCompat import androidx.media.MediaBrowserServiceCompat.Result import androidx.media.utils.MediaConstants @@ -103,7 +104,7 @@ class MediaBrowserImpl( private fun onLoadChildren(parentId: String): Single> { try { - val parentIdUri = Uri.parse(parentId) + val parentIdUri = parentId.toUri() val path = ArrayList(parentIdUri.pathSegments) if (path.isEmpty()) { @@ -185,7 +186,7 @@ class MediaBrowserImpl( builder .setMediaId(createMediaIdForInfoItem(playlist is PlaylistRemoteEntity, playlist.uid)) .setTitle(playlist.orderingName) - .setIconUri(playlist.thumbnailUrl?.let { Uri.parse(it) }) + .setIconUri(imageUriOrNullIfDisabled(playlist.thumbnailUrl)) val extras = Bundle() extras.putString( @@ -212,7 +213,7 @@ class MediaBrowserImpl( } ImageStrategy.choosePreferredImage(item.thumbnails)?.let { - builder.setIconUri(Uri.parse(it)) + builder.setIconUri(imageUriOrNullIfDisabled(it)) } return MediaBrowserCompat.MediaItem( @@ -258,7 +259,7 @@ class MediaBrowserImpl( builder.setMediaId(createMediaIdForPlaylistIndex(false, playlistId, index)) .setTitle(item.streamEntity.title) .setSubtitle(item.streamEntity.uploader) - .setIconUri(Uri.parse(item.streamEntity.thumbnailUrl)) + .setIconUri(imageUriOrNullIfDisabled(item.streamEntity.thumbnailUrl)) return MediaBrowserCompat.MediaItem( builder.build(), @@ -277,7 +278,7 @@ class MediaBrowserImpl( .setSubtitle(item.uploaderName) ImageStrategy.choosePreferredImage(item.thumbnails)?.let { - builder.setIconUri(Uri.parse(it)) + builder.setIconUri(imageUriOrNullIfDisabled(it)) } return MediaBrowserCompat.MediaItem( @@ -316,7 +317,7 @@ class MediaBrowserImpl( builder.setMediaId(mediaId) .setTitle(streamHistoryEntry.streamEntity.title) .setSubtitle(streamHistoryEntry.streamEntity.uploader) - .setIconUri(Uri.parse(streamHistoryEntry.streamEntity.thumbnailUrl)) + .setIconUri(imageUriOrNullIfDisabled(streamHistoryEntry.streamEntity.thumbnailUrl)) return MediaBrowserCompat.MediaItem( builder.build(), @@ -395,5 +396,13 @@ class MediaBrowserImpl( companion object { private val TAG: String = MediaBrowserImpl::class.java.getSimpleName() + + fun imageUriOrNullIfDisabled(url: String?): Uri? { + return if (ImageStrategy.shouldLoadImages()) { + url?.toUri() + } else { + null + } + } } } diff --git a/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserPlaybackPreparer.kt b/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserPlaybackPreparer.kt index f34677a29..a3791e2e7 100644 --- a/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserPlaybackPreparer.kt +++ b/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserPlaybackPreparer.kt @@ -6,6 +6,7 @@ import android.os.Bundle import android.os.ResultReceiver import android.support.v4.media.session.PlaybackStateCompat import android.util.Log +import androidx.core.net.toUri import com.google.android.exoplayer2.Player import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector.PlaybackPreparer import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers @@ -137,7 +138,7 @@ class MediaBrowserPlaybackPreparer( private fun extractPlayQueueFromMediaId(mediaId: String): Single { try { - val mediaIdUri = Uri.parse(mediaId) + val mediaIdUri = mediaId.toUri() val path = ArrayList(mediaIdUri.pathSegments) if (path.isEmpty()) { throw parseError(mediaId) diff --git a/app/src/main/java/org/schabi/newpipe/settings/SelectFeedGroupFragment.java b/app/src/main/java/org/schabi/newpipe/settings/SelectFeedGroupFragment.java new file mode 100644 index 000000000..662379369 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/settings/SelectFeedGroupFragment.java @@ -0,0 +1,214 @@ +package org.schabi.newpipe.settings; + +import android.content.DialogInterface; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.ProgressBar; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.DialogFragment; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import org.schabi.newpipe.NewPipeDatabase; +import org.schabi.newpipe.R; +import org.schabi.newpipe.database.AppDatabase; +import org.schabi.newpipe.database.feed.model.FeedGroupEntity; +import org.schabi.newpipe.error.ErrorUtil; +import org.schabi.newpipe.util.ThemeHelper; + +import java.util.List; +import java.util.Vector; + +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; +import io.reactivex.rxjava3.core.Observer; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.schedulers.Schedulers; + +/** + * Created by Christian Schabesberger on 26.09.17. + * SelectChannelFragment.java is part of NewPipe. + *

+ * NewPipe is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + *

+ *

+ * NewPipe is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + *

+ *

+ * You should have received a copy of the GNU General Public License + * along with NewPipe. If not, see . + *

+ */ + +public class SelectFeedGroupFragment extends DialogFragment { + + private OnSelectedListener onSelectedListener = null; + private OnCancelListener onCancelListener = null; + + private ProgressBar progressBar; + private TextView emptyView; + private RecyclerView recyclerView; + + private List feedGroups = new Vector<>(); + + public void setOnSelectedListener(final OnSelectedListener listener) { + onSelectedListener = listener; + } + + public void setOnCancelListener(final OnCancelListener listener) { + onCancelListener = listener; + } + + /*////////////////////////////////////////////////////////////////////////// + // Init + //////////////////////////////////////////////////////////////////////////*/ + + @Override + public void onCreate(@Nullable final Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setStyle(STYLE_NO_TITLE, ThemeHelper.getMinWidthDialogTheme(requireContext())); + } + + @Override + public View onCreateView(@NonNull final LayoutInflater inflater, final ViewGroup container, + final Bundle savedInstanceState) { + final View v = inflater.inflate(R.layout.select_feed_group_fragment, container, false); + recyclerView = v.findViewById(R.id.items_list); + recyclerView.setLayoutManager(new LinearLayoutManager(getContext())); + final SelectFeedGroupAdapter feedGroupAdapter = new SelectFeedGroupAdapter(); + recyclerView.setAdapter(feedGroupAdapter); + + progressBar = v.findViewById(R.id.progressBar); + emptyView = v.findViewById(R.id.empty_state_view); + progressBar.setVisibility(View.VISIBLE); + recyclerView.setVisibility(View.GONE); + emptyView.setVisibility(View.GONE); + + + final AppDatabase database = NewPipeDatabase.getInstance(requireContext()); + database.feedGroupDAO().getAll().toObservable() + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(getFeedGroupObserver()); + + return v; + } + + /*////////////////////////////////////////////////////////////////////////// + // Handle actions + //////////////////////////////////////////////////////////////////////////*/ + + @Override + public void onCancel(@NonNull final DialogInterface dialogInterface) { + super.onCancel(dialogInterface); + if (onCancelListener != null) { + onCancelListener.onCancel(); + } + } + + private void clickedItem(final int position) { + if (onSelectedListener != null) { + final FeedGroupEntity entry = feedGroups.get(position); + onSelectedListener + .onFeedGroupSelected(entry.getUid(), entry.getName(), + entry.getIcon().getDrawableResource()); + } + dismiss(); + } + + /*////////////////////////////////////////////////////////////////////////// + // Item handling + //////////////////////////////////////////////////////////////////////////*/ + + private void displayFeedGroups(final List newFeedGroups) { + this.feedGroups = newFeedGroups; + progressBar.setVisibility(View.GONE); + if (newFeedGroups.isEmpty()) { + emptyView.setVisibility(View.VISIBLE); + return; + } + recyclerView.setVisibility(View.VISIBLE); + + } + + private Observer> getFeedGroupObserver() { + return new Observer>() { + @Override + public void onSubscribe(@NonNull final Disposable disposable) { } + + @Override + public void onNext(@NonNull final List newGroups) { + displayFeedGroups(newGroups); + } + + @Override + public void onError(@NonNull final Throwable exception) { + ErrorUtil.showUiErrorSnackbar(SelectFeedGroupFragment.this, + "Loading Feed Groups", exception); + } + + @Override + public void onComplete() { } + }; + } + + /*////////////////////////////////////////////////////////////////////////// + // Interfaces + //////////////////////////////////////////////////////////////////////////*/ + + public interface OnSelectedListener { + void onFeedGroupSelected(Long groupId, String name, int icon); + } + + public interface OnCancelListener { + void onCancel(); + } + + private class SelectFeedGroupAdapter + extends RecyclerView.Adapter { + @NonNull + @Override + public SelectFeedGroupItemHolder onCreateViewHolder(final ViewGroup parent, + final int viewType) { + final View item = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.select_feed_group_item, parent, false); + return new SelectFeedGroupItemHolder(item); + } + + @Override + public void onBindViewHolder(final SelectFeedGroupItemHolder holder, final int position) { + final FeedGroupEntity entry = feedGroups.get(position); + holder.titleView.setText(entry.getName()); + holder.view.setOnClickListener(view -> clickedItem(position)); + holder.thumbnailView.setImageResource(entry.getIcon().getDrawableResource()); + } + + @Override + public int getItemCount() { + return feedGroups.size(); + } + + public class SelectFeedGroupItemHolder extends RecyclerView.ViewHolder { + public final View view; + final ImageView thumbnailView; + final TextView titleView; + SelectFeedGroupItemHolder(final View v) { + super(v); + this.view = v; + thumbnailView = v.findViewById(R.id.itemThumbnailView); + titleView = v.findViewById(R.id.itemTitleView); + } + } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/settings/tabs/ChooseTabsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/tabs/ChooseTabsFragment.java index 289c824ba..738a9c926 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/tabs/ChooseTabsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/tabs/ChooseTabsFragment.java @@ -34,6 +34,7 @@ import org.schabi.newpipe.error.UserAction; import org.schabi.newpipe.settings.SelectChannelFragment; import org.schabi.newpipe.settings.SelectKioskFragment; import org.schabi.newpipe.settings.SelectPlaylistFragment; +import org.schabi.newpipe.settings.SelectFeedGroupFragment; import org.schabi.newpipe.settings.tabs.AddTabDialog.ChooseTabListItem; import org.schabi.newpipe.util.ThemeHelper; @@ -203,6 +204,14 @@ public class ChooseTabsFragment extends Fragment { }); selectPlaylistFragment.show(getParentFragmentManager(), "select_playlist"); return; + case FEEDGROUP: + final SelectFeedGroupFragment selectFeedGroupFragment = + new SelectFeedGroupFragment(); + selectFeedGroupFragment.setOnSelectedListener( + (groupId, name, iconId) -> + addTab(new Tab.FeedGroupTab(groupId, name, iconId))); + selectFeedGroupFragment.show(getParentFragmentManager(), "select_feed_group"); + return; default: addTab(type.getTab()); break; @@ -244,6 +253,11 @@ public class ChooseTabsFragment extends Fragment { getString(R.string.playlist_page_summary), tab.getTabIconRes(context))); break; + case FEEDGROUP: + returnList.add(new ChooseTabListItem(tab.getTabId(), + getString(R.string.feed_group_page_summary), + tab.getTabIconRes(context))); + break; default: if (!tabList.contains(tab)) { returnList.add(new ChooseTabListItem(context, tab)); @@ -396,6 +410,9 @@ public class ChooseTabsFragment extends Fragment { ? getString(R.string.local) : getNameOfServiceById(serviceId); return serviceName + "/" + tab.getTabName(requireContext()); + case FEEDGROUP: + return getString(R.string.feed_groups_header_title) + + "/" + ((Tab.FeedGroupTab) tab).getFeedGroupName(); default: return tab.getTabName(requireContext()); } diff --git a/app/src/main/java/org/schabi/newpipe/settings/tabs/Tab.java b/app/src/main/java/org/schabi/newpipe/settings/tabs/Tab.java index bf1503dee..3e642ab0f 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/tabs/Tab.java +++ b/app/src/main/java/org/schabi/newpipe/settings/tabs/Tab.java @@ -93,6 +93,8 @@ public abstract class Tab { return new ChannelTab(jsonObject); case PLAYLIST: return new PlaylistTab(jsonObject); + case FEEDGROUP: + return new FeedGroupTab(jsonObject); } } @@ -162,7 +164,8 @@ public abstract class Tab { HISTORY(new HistoryTab()), KIOSK(new KioskTab()), CHANNEL(new ChannelTab()), - PLAYLIST(new PlaylistTab()); + PLAYLIST(new PlaylistTab()), + FEEDGROUP(new FeedGroupTab()); private final Tab tab; @@ -458,7 +461,7 @@ public abstract class Tab { final ChannelTab other = (ChannelTab) obj; return super.equals(obj) && channelServiceId == other.channelServiceId - && channelUrl.equals(other.channelName) + && channelUrl.equals(other.channelUrl) && channelName.equals(other.channelName); } @@ -649,4 +652,93 @@ public abstract class Tab { return playlistType; } } + public static class FeedGroupTab extends Tab { + public static final int ID = 9; + private static final String JSON_FEED_GROUP_ID_KEY = "feed_group_id"; + private static final String JSON_FEED_GROUP_NAME_KEY = "feed_group_name"; + private static final String JSON_FEED_GROUP_ICON_KEY = "feed_group_icon"; + private Long feedGroupId; + private String feedGroupName; + private int iconId; + + private FeedGroupTab() { + this((long) -1, NO_NAME, R.drawable.ic_asterisk); + } + + public FeedGroupTab(final Long feedGroupId, final String feedGroupName, + final int iconId) { + this.feedGroupId = feedGroupId; + this.feedGroupName = feedGroupName; + this.iconId = iconId; + + } + + public FeedGroupTab(final JsonObject jsonObject) { + super(jsonObject); + } + + @Override + public int getTabId() { + return ID; + } + + @Override + public String getTabName(final Context context) { + return context.getString(R.string.fragment_feed_title); + } + + @DrawableRes + @Override + public int getTabIconRes(final Context context) { + return this.iconId; + } + + @Override + public FeedFragment getFragment(final Context context) { + return FeedFragment.newInstance(feedGroupId, feedGroupName); + } + + @Override + protected void writeDataToJson(final JsonStringWriter writerSink) { + writerSink.value(JSON_FEED_GROUP_ID_KEY, feedGroupId) + .value(JSON_FEED_GROUP_NAME_KEY, feedGroupName) + .value(JSON_FEED_GROUP_ICON_KEY, iconId); + } + + @Override + protected void readDataFromJson(final JsonObject jsonObject) { + feedGroupId = jsonObject.getLong(JSON_FEED_GROUP_ID_KEY, -1); + feedGroupName = jsonObject.getString(JSON_FEED_GROUP_NAME_KEY, NO_NAME); + iconId = jsonObject.getInt(JSON_FEED_GROUP_ICON_KEY, R.drawable.ic_asterisk); + } + + @Override + public boolean equals(final Object obj) { + if (!(obj instanceof FeedGroupTab)) { + return false; + } + final FeedGroupTab other = (FeedGroupTab) obj; + return super.equals(obj) + && feedGroupId.equals(other.feedGroupId) + && feedGroupName.equals(other.feedGroupName) + && iconId == other.iconId; + } + + @Override + public int hashCode() { + return Objects.hash(getTabId(), feedGroupId, feedGroupName, iconId); + } + + public Long getFeedGroupId() { + return feedGroupId; + } + + public String getFeedGroupName() { + return feedGroupName; + } + + public int getIconId() { + return iconId; + } + } } diff --git a/app/src/main/java/org/schabi/newpipe/util/Localization.java b/app/src/main/java/org/schabi/newpipe/util/Localization.java index 8c7b30a9b..825bee343 100644 --- a/app/src/main/java/org/schabi/newpipe/util/Localization.java +++ b/app/src/main/java/org/schabi/newpipe/util/Localization.java @@ -90,19 +90,14 @@ public final class Localization { * Localize a user name like @foobar. * * Will correctly handle right-to-left usernames by using a {@link BidiFormatter}. + * For right-to-left usernames, it will put the @ on the right side to read more naturally. * * @param plainName username, with an optional leading @ * @return a usernames that can include RTL-characters */ @NonNull public static String localizeUserName(final String plainName) { - final BidiFormatter bidi = BidiFormatter.getInstance(); - - if (plainName.startsWith("@")) { - return "@" + bidi.unicodeWrap(plainName.substring(1)); - } else { - return bidi.unicodeWrap(plainName); - } + return BidiFormatter.getInstance().unicodeWrap(plainName); } public static org.schabi.newpipe.extractor.localization.Localization getPreferredLocalization( diff --git a/app/src/main/res/layout/select_feed_group_fragment.xml b/app/src/main/res/layout/select_feed_group_fragment.xml new file mode 100644 index 000000000..bb17d5f6e --- /dev/null +++ b/app/src/main/res/layout/select_feed_group_fragment.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/layout/select_feed_group_item.xml b/app/src/main/res/layout/select_feed_group_item.xml new file mode 100644 index 000000000..ccce555f5 --- /dev/null +++ b/app/src/main/res/layout/select_feed_group_item.xml @@ -0,0 +1,38 @@ + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 68967074a..ef0522044 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -412,6 +412,8 @@ Do you want to also import settings? Could not load comments The language will change once the app is restarted + Select a feed group + No feed group created yet Trending Top 50 @@ -686,6 +688,7 @@ What\'s New + Channel group page Channel groups Feed last updated: %s Not loaded: %d diff --git a/app/src/test/java/org/schabi/newpipe/settings/tabs/TabsJsonHelperTest.java b/app/src/test/java/org/schabi/newpipe/settings/tabs/TabsJsonHelperTest.java index bddb130fe..561a8cbec 100644 --- a/app/src/test/java/org/schabi/newpipe/settings/tabs/TabsJsonHelperTest.java +++ b/app/src/test/java/org/schabi/newpipe/settings/tabs/TabsJsonHelperTest.java @@ -93,9 +93,11 @@ public class TabsJsonHelperTest { final Tab.ChannelTab channelTab = new Tab.ChannelTab( 666, "https://example.org", "testName"); final Tab.KioskTab kioskTab = new Tab.KioskTab(123, "trending_key"); + final Tab.FeedGroupTab feedGroupTab = new Tab.FeedGroupTab( + 1L, "x", 123); final List tabs = Arrays.asList( - blankTab, defaultKioskTab, subscriptionsTab, channelTab, kioskTab); + blankTab, defaultKioskTab, subscriptionsTab, channelTab, kioskTab, feedGroupTab); final String returnedJson = TabsJsonHelper.getJsonToSave(tabs); // Reading @@ -130,5 +132,13 @@ public class TabsJsonHelperTest { assertEquals(kioskTab.getTabId(), kioskTabFromReturnedJson.getTabId()); assertEquals(kioskTab.getKioskServiceId(), kioskTabFromReturnedJson.getKioskServiceId()); assertEquals(kioskTab.getKioskId(), kioskTabFromReturnedJson.getKioskId()); + + final Tab.FeedGroupTab grpTabFromReturnedJson = requireNonNull( + (Tab.FeedGroupTab) Tab.from((JsonObject) tabsFromArray.get(5) + )); + assertEquals(feedGroupTab.getTabId(), grpTabFromReturnedJson.getTabId()); + assertEquals(feedGroupTab.getFeedGroupId(), grpTabFromReturnedJson.getFeedGroupId()); + assertEquals(feedGroupTab.getIconId(), grpTabFromReturnedJson.getIconId()); + assertEquals(feedGroupTab.getFeedGroupName(), grpTabFromReturnedJson.getFeedGroupName()); } } diff --git a/doc/README.ar.md b/doc/README.ar.md index 242516cdc..f005050c7 100644 --- a/doc/README.ar.md +++ b/doc/README.ar.md @@ -2,15 +2,26 @@

NewPipe

.Android واجهة أمامية متدفقة خفيفة الوزن لنظام

-

Get it on F-Droid

+

Get it on F-Droid

+ + + + + + - + -

+ +

+ + +

+

لقطات الشاشةالخدمات المدعومةوصفسماتالتثبيت والتحديثاتمساهمةالتبرعاترخصة

موقعمدونةالأسئلة الشائعةإضغط

diff --git a/doc/README.asm.md b/doc/README.asm.md index 8042b3db9..37d0949b7 100644 --- a/doc/README.asm.md +++ b/doc/README.asm.md @@ -2,16 +2,26 @@

NewPipe

এণ্ড্ৰইডৰ বাবে এটা লিব্ৰে লাইটৱেট ষ্ট্ৰীমিং ফ্ৰন্ট-এণ্ড।

-

Get it on F-Droid

+

Get it on F-Droid

+ + + + + + - + +

+ +

+

স্ক্ৰীণশ্বটসমৰ্থিত সেৱাসমূহবিৱৰণ • diff --git a/doc/README.de.md b/doc/README.de.md index 5b3275d07..34ad94ab1 100644 --- a/doc/README.de.md +++ b/doc/README.de.md @@ -1,19 +1,30 @@ -

Wir planen große Teile des Quellcodes neu zu schreiben, um NewPipe neu, modern und stabiler zu machen!

-

Öffne keine neuen Pull Requests für neue Features, es werden nur Fehlerbehebungen akzeptiert.

+

Wir sind im Prozess, größere Teile unseres Codes neuzuschreiben, um eine moderne und stabile NewPipe App zu kreieren! Du kannst nightly builds hier herunterladen.

+

Bitte nutze den refactor branch als Arbeitsgrundlage, wenn du neue Funktionen beitragen willst. Die aktuelle Codebase ist im reinen Maintenance mode und bekommt nur noch Fehlerbehebungen.

NewPipe

Eine freie, offene und leichtgewichtige Streaming App für Android.

-

Hole es dir auf F-Droid

+

Hole es dir auf F-Droid

+ + + + + + - + -

+ +

+ + +

+

ScreenshotsUnterstützte DiensteBeschreibungFeaturesInstallation und UpdatesBeitragSpendenLizenz

WebsiteBlogFAQÜber NewPipe

diff --git a/doc/README.es.md b/doc/README.es.md index 8ec58e771..4a08cba08 100644 --- a/doc/README.es.md +++ b/doc/README.es.md @@ -2,15 +2,26 @@

NewPipe

Una interfaz de streaming ligera y libre para Android.

-

Get it on F-Droid

+

Get it on F-Droid

+ + + + + + - + -

+ +

+ + +

+

Capturas de PantallaDescripciónCaracterísticasInstalación y ActualizacionesContribuciónDonarLicencia

diff --git a/doc/README.fr.md b/doc/README.fr.md index 772f4a1ae..cfebcb2a6 100644 --- a/doc/README.fr.md +++ b/doc/README.fr.md @@ -5,15 +5,26 @@

NewPipe

Un front-end de streaming libre et léger pour Android.

-

Get it on F-Droid

+

Get it on F-Droid

+ + + + + + - + -

+ +

+ + +

+

Captures d'écranServices SupportésDescriptionFonctionnalitésInstallation et mises à jourContribuerDonsLicence

SiteBlogFAQPresse

diff --git a/doc/README.hi.md b/doc/README.hi.md index 37ae71a4a..6098c6c26 100644 --- a/doc/README.hi.md +++ b/doc/README.hi.md @@ -2,16 +2,26 @@

NewPipe

Android के लिए एक ओपन सोर्स, हल्का YouTube ऐप।

-

इसे F-Droid पर पाएँ

+

इसे F-Droid पर पाएँ

+ + + + + + - + +

+ +

+

ऐप कैसी दिखती हैसमर्थित सेवाएँविवरणसुविधाएँस्थापित करना और अपडेट करनायोगदान करेंआर्थिक योगदान करेंलाइसेंस

वेबसाइटब्लॉगसाधारण सवाल-जवाबप्रेस

diff --git a/doc/README.it.md b/doc/README.it.md index 6c227ea2f..d926db6bc 100644 --- a/doc/README.it.md +++ b/doc/README.it.md @@ -2,15 +2,26 @@

NewPipe

Un frontend di streaming libero e leggero per Android.

-

Scaricalo su F-Droid

+

Scaricalo su F-Droid

+ + + + + + - + -

+ +

+ + +

+

ScreenshotServizi SupportatiDescrizioneFunzionalitàInstallazione e aggiornamentiContribuireDonareLicenza

SitoBlogFAQStampa

diff --git a/doc/README.ja.md b/doc/README.ja.md index e8f708a8a..1e751855b 100644 --- a/doc/README.ja.md +++ b/doc/README.ja.md @@ -2,15 +2,26 @@

NewPipe

自由で軽量な Android 向けストリーミングフロントエンド

-

Get it on F-Droid

+

Get it on F-Droid

+ + + + + + - + -

+ +

+ + +

+

スクリーンショット説明機能インストールと更新貢献寄付ライセンス

ウェブサイトブログFAQニュース

diff --git a/doc/README.ko.md b/doc/README.ko.md index 3215bd713..39fb7e11c 100644 --- a/doc/README.ko.md +++ b/doc/README.ko.md @@ -2,15 +2,26 @@

NewPipe

A libre lightweight streaming frontend for Android.

-

Get it on F-Droid

+

Get it on F-Droid

+ + + + + + - + -

+ +

+ + +

+

ScreenshotsDescriptionFeaturesUpdatesContributionDonateLicense

WebsiteBlogFAQPress

diff --git a/doc/README.pa.md b/doc/README.pa.md index 0e254adf1..9b84ded18 100644 --- a/doc/README.pa.md +++ b/doc/README.pa.md @@ -2,16 +2,26 @@

NewPipe

ਐਂਡਰੌਇਡ ਲਈ ਇੱਕ ਮੁਫ਼ਤ ਹਲਕਾ-ਫੁਲਕਾ ਸਟ੍ਰੀਮਿੰਗ ਯੂਟਿਊਬ ਫਰੰਟ-ਐਂਡ।

-

Get it on F-Droid

+

Get it on F-Droid

+ + + + + + - + +

+ +

+

ਐਪ ਕਿਹੋ-ਜਿਹੀ ਦਿਖਦੀ ਹੈਸਮਰਥਿਤ ਸੇਵਾਵਾਂਵਰਣਨਵਿਸ਼ੇਸ਼ਤਾਵਾਂਇੰਸਟਾਲੇਸ਼ਨ ਅਤੇ ਅੱਪਡੇਟਯੋਗਦਾਨਦਾਨਲਾਈਸੈਂਸ

ਵੈੱਬਸਾਈਟਬਲੌਗਆਮ ਸਵਾਲ ਜਵਾਬਪ੍ਰੈਸ

diff --git a/doc/README.pl.md b/doc/README.pl.md index 96d493153..9574491c7 100644 --- a/doc/README.pl.md +++ b/doc/README.pl.md @@ -2,15 +2,26 @@

NewPipe

Wolny, lekki streamingowy frontend na Androida.

-

Pobierz z F-Droid

+

Pobierz z F-Droid

+ + + + + + - + -

+ +

+ + +

+

ScreenshotyOpisFunkcjeInstalacja i aktualizacjeWkładWesprzyjLicencja

StronaBlogFAQPress

diff --git a/doc/README.pt_BR.md b/doc/README.pt_BR.md index da6c4fce6..b73da2de1 100644 --- a/doc/README.pt_BR.md +++ b/doc/README.pt_BR.md @@ -6,15 +6,26 @@

NewPipe

Uma interface de streaming leve e gratuita para Android.

-

Get it on F-Droid

+

Get it on F-Droid

+ + + + + + - + -

+ +

+ + +

+

ScreenshotsServiços SuportadosDescriçãoRecursosInstalação e atualizaçõesContribuiçõesDoarLicença

SiteBlogFAQPress

diff --git a/doc/README.ro.md b/doc/README.ro.md index 29c1d3666..3f146f7e4 100644 --- a/doc/README.ro.md +++ b/doc/README.ro.md @@ -2,15 +2,26 @@

NewPipe

Un front-end de streaming „uşor” liber, pentru Android.

-

Get it on F-Droid

+

Get it on F-Droid

+ + + + + + - + -

+ +

+ + +

+

Capturi de ecranDescriereFuncţiiInstalare şi actualizăriContribuţieDonaţiLicenţă

WebsiteBlogFAQPresă

diff --git a/doc/README.ru.md b/doc/README.ru.md index e3c76d329..8a9955707 100644 --- a/doc/README.ru.md +++ b/doc/README.ru.md @@ -2,15 +2,26 @@

NewPipe

Свободный и легковесный клиент потоковых сервисов для Android.

-

Скачать на F-Droid

+

Скачать на F-Droid

+ + + + + + - + -

+ +

+ + +

+

СкриншотыПоддерживаемые сервисыОписаниеВозможностиУстановка и обновленияУчастиеПожертвованиеЛицензия

СайтБлогЧЗВПресса

diff --git a/doc/README.ryu.md b/doc/README.ryu.md index 2e24aa41c..f3ca31af0 100644 --- a/doc/README.ryu.md +++ b/doc/README.ryu.md @@ -2,15 +2,26 @@

NewPipe

じゆーいっしけいりょうなAndroidんきーストリーミングフロントエンド

-

Get it on F-Droid

+

Get it on F-Droid

+ + + + + + - + -

+ +

+ + +

+

スクリーンショットしちめいちぬーインストールとぅこうしんこうきんちーふライセンス

ウェブサイトブログFAQニュース

diff --git a/doc/README.so.md b/doc/README.so.md index 640feae60..843bed749 100644 --- a/doc/README.so.md +++ b/doc/README.so.md @@ -2,15 +2,26 @@

NewPipe

App bilaash ah oo fudud looguna talagalay in Android-ka wax loogu daawado.

-

Get it on F-Droid

+

Get it on F-Droid

+ + + + + + - + -

+ +

+ + +

+

Sawir-shaashadeedFaahfaahinWaxqabadkaKushubida iyo cusboonaysiintaKusoo KordhinUgu DeeqLaysinka

Website-kaMaqaaladaSu'aalaha Aalaa La-iswaydiiyoWarbaahinta

diff --git a/doc/README.sr.md b/doc/README.sr.md index 1a9118638..21e4d857c 100644 --- a/doc/README.sr.md +++ b/doc/README.sr.md @@ -5,15 +5,26 @@

NewPipe

Бесплатна и лагана апликација за стримовање за Android.

-

Набавите на F-Droid

+

Набавите на F-Droid

+ + + + + + - + -

+ +

+ + +

+

Снимци екранаПодржане услугеОписКарактеристикеИнсталација и ажурирањаДоприносДонацијаЛиценца

Веб-сајтБлогЧППШтампа

diff --git a/doc/README.tr.md b/doc/README.tr.md index bbdd85f76..6e95e54de 100644 --- a/doc/README.tr.md +++ b/doc/README.tr.md @@ -2,15 +2,26 @@

NewPipe

Android için hafif ve özgür bir akış arayüzü.

-

Get it on F-Droid

+

Get it on F-Droid

+ + + + + + - + -

+ +

+ + +

+

Ekran fotoğraflarıAçıklamaÖzelliklerKurulum ve güncellemelerKatkıda bulunmaBağışLisans

Web sitesiBlogSSSBasın

diff --git a/doc/README.zh_TW.md b/doc/README.zh_TW.md index 760a43ad5..05518624f 100644 --- a/doc/README.zh_TW.md +++ b/doc/README.zh_TW.md @@ -2,15 +2,26 @@

NewPipe

輕巧的 Android 串流前端

-

Get it on F-Droid

+

Get it on F-Droid

+ + + + + + - + -

+ +

+ + +

+

截圖說明功能安裝與更新貢獻捐款授權憑證

網站部落格FAQ媒體