1
0
mirror of https://github.com/TeamNewPipe/NewPipe synced 2025-01-09 17:00:32 +00:00

Remove old playlist fragment

This commit is contained in:
Isira Seneviratne 2024-07-13 10:54:15 +05:30
parent 3da4aeee29
commit 9dfd064d86
7 changed files with 34 additions and 542 deletions

View File

@ -768,7 +768,7 @@ public class MainActivity extends AppCompatActivity {
break;
case PLAYLIST:
NavigationHelper.openPlaylistFragment(getSupportFragmentManager(),
serviceId, url, title);
serviceId, url);
break;
}
} else if (intent.hasExtra(Constants.KEY_OPEN_SEARCH)) {

View File

@ -279,7 +279,7 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I>
try {
onItemSelected(selectedItem);
NavigationHelper.openPlaylistFragment(getFM(), selectedItem.getServiceId(),
selectedItem.getUrl(), selectedItem.getName());
selectedItem.getUrl());
} catch (final Exception e) {
ErrorUtil.showUiErrorSnackbar(this, "Opening playlist fragment", e);
}

View File

@ -1,514 +1,35 @@
package org.schabi.newpipe.fragments.list.playlist;
package org.schabi.newpipe.fragments.list.playlist
import static org.schabi.newpipe.extractor.utils.Utils.isBlank;
import static org.schabi.newpipe.ktx.ViewUtils.animate;
import static org.schabi.newpipe.ktx.ViewUtils.animateHideRecyclerViewAllowingScrolling;
import static org.schabi.newpipe.util.ServiceHelper.getServiceById;
import android.os.Bundle
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.core.os.bundleOf
import androidx.fragment.app.Fragment
import org.schabi.newpipe.compose.playlist.Playlist
import org.schabi.newpipe.compose.theme.AppTheme
import org.schabi.newpipe.util.KEY_SERVICE_ID
import org.schabi.newpipe.util.KEY_URL
import android.content.Context;
import android.os.Bundle;
import android.text.TextUtils;
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.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.content.res.AppCompatResources;
import com.google.android.material.shape.CornerFamily;
import com.google.android.material.shape.ShapeAppearanceModel;
import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription;
import org.schabi.newpipe.NewPipeDatabase;
import org.schabi.newpipe.R;
import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity;
import org.schabi.newpipe.database.stream.model.StreamEntity;
import org.schabi.newpipe.databinding.PlaylistControlBinding;
import org.schabi.newpipe.databinding.PlaylistHeaderBinding;
import org.schabi.newpipe.error.ErrorInfo;
import org.schabi.newpipe.error.ErrorUtil;
import org.schabi.newpipe.error.UserAction;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.ListExtractor;
import org.schabi.newpipe.extractor.ServiceList;
import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
import org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper;
import org.schabi.newpipe.extractor.stream.Description;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
import org.schabi.newpipe.info_list.dialog.InfoItemDialog;
import org.schabi.newpipe.info_list.dialog.StreamDialogDefaultEntry;
import org.schabi.newpipe.local.dialog.PlaylistDialog;
import org.schabi.newpipe.local.playlist.RemotePlaylistManager;
import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.player.playqueue.PlaylistPlayQueue;
import org.schabi.newpipe.util.ExtractorHelper;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.PlayButtonHelper;
import org.schabi.newpipe.util.external_communication.ShareUtils;
import org.schabi.newpipe.util.image.CoilHelper;
import org.schabi.newpipe.util.text.TextEllipsizer;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import coil.util.CoilUtils;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.core.Flowable;
import io.reactivex.rxjava3.core.Single;
import io.reactivex.rxjava3.disposables.CompositeDisposable;
import io.reactivex.rxjava3.disposables.Disposable;
public class PlaylistFragment extends BaseListInfoFragment<StreamInfoItem, PlaylistInfo>
implements PlaylistControlViewHolder {
private CompositeDisposable disposables;
private Subscription bookmarkReactor;
private AtomicBoolean isBookmarkButtonReady;
private RemotePlaylistManager remotePlaylistManager;
private PlaylistRemoteEntity playlistEntity;
/*//////////////////////////////////////////////////////////////////////////
// Views
//////////////////////////////////////////////////////////////////////////*/
private PlaylistHeaderBinding headerBinding;
private PlaylistControlBinding playlistControlBinding;
private MenuItem playlistBookmarkButton;
private long streamCount;
private long playlistOverallDurationSeconds;
public static PlaylistFragment getInstance(final int serviceId, final String url,
final String name) {
final PlaylistFragment instance = new PlaylistFragment();
instance.setInitialData(serviceId, url, name);
return instance;
}
public PlaylistFragment() {
super(UserAction.REQUESTED_PLAYLIST);
}
/*//////////////////////////////////////////////////////////////////////////
// LifeCycle
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
disposables = new CompositeDisposable();
isBookmarkButtonReady = new AtomicBoolean(false);
remotePlaylistManager = new RemotePlaylistManager(NewPipeDatabase
.getInstance(requireContext()));
}
@Override
public View onCreateView(@NonNull final LayoutInflater inflater,
@Nullable final ViewGroup container,
@Nullable final Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_playlist, container, false);
}
/*//////////////////////////////////////////////////////////////////////////
// Init
//////////////////////////////////////////////////////////////////////////*/
@Override
protected Supplier<View> getListHeaderSupplier() {
headerBinding = PlaylistHeaderBinding
.inflate(activity.getLayoutInflater(), itemsList, false);
playlistControlBinding = headerBinding.playlistControl;
return headerBinding::getRoot;
}
@Override
protected void initViews(final View rootView, final Bundle savedInstanceState) {
super.initViews(rootView, savedInstanceState);
// Is mini variant still relevant?
// Only the remote playlist screen uses it now
infoListAdapter.setUseMiniVariant(true);
}
private PlayQueue getPlayQueueStartingAt(final StreamInfoItem infoItem) {
return getPlayQueue(Math.max(infoListAdapter.getItemsList().indexOf(infoItem), 0));
}
@Override
protected void showInfoItemDialog(final StreamInfoItem item) {
final Context context = getContext();
try {
final InfoItemDialog.Builder dialogBuilder =
new InfoItemDialog.Builder(getActivity(), context, this, item);
dialogBuilder
.setAction(
StreamDialogDefaultEntry.START_HERE_ON_BACKGROUND,
(f, infoItem) -> NavigationHelper.playOnBackgroundPlayer(
context, getPlayQueueStartingAt(infoItem), true))
.create()
.show();
} catch (final IllegalArgumentException e) {
InfoItemDialog.Builder.reportErrorDuringInitialization(e, item);
}
}
@Override
public void onCreateOptionsMenu(@NonNull final Menu menu,
@NonNull final MenuInflater inflater) {
if (DEBUG) {
Log.d(TAG, "onCreateOptionsMenu() called with: "
+ "menu = [" + menu + "], inflater = [" + inflater + "]");
}
super.onCreateOptionsMenu(menu, inflater);
inflater.inflate(R.menu.menu_playlist, menu);
playlistBookmarkButton = menu.findItem(R.id.menu_item_bookmark);
updateBookmarkButtons();
}
@Override
public void onDestroyView() {
headerBinding = null;
playlistControlBinding = null;
super.onDestroyView();
if (isBookmarkButtonReady != null) {
isBookmarkButtonReady.set(false);
}
if (disposables != null) {
disposables.clear();
}
if (bookmarkReactor != null) {
bookmarkReactor.cancel();
}
bookmarkReactor = null;
}
@Override
public void onDestroy() {
super.onDestroy();
if (disposables != null) {
disposables.dispose();
}
disposables = null;
remotePlaylistManager = null;
playlistEntity = null;
isBookmarkButtonReady = null;
}
/*//////////////////////////////////////////////////////////////////////////
// Load and handle
//////////////////////////////////////////////////////////////////////////*/
@Override
protected Single<ListExtractor.InfoItemsPage<StreamInfoItem>> loadMoreItemsLogic() {
return ExtractorHelper.getMorePlaylistItems(serviceId, url, currentNextPage);
}
@Override
protected Single<PlaylistInfo> loadResult(final boolean forceLoad) {
return ExtractorHelper.getPlaylistInfo(serviceId, url, forceLoad);
}
@Override
public boolean onOptionsItemSelected(final MenuItem item) {
switch (item.getItemId()) {
case R.id.action_settings:
NavigationHelper.openSettings(requireContext());
break;
case R.id.menu_item_openInBrowser:
ShareUtils.openUrlInBrowser(requireContext(), url);
break;
case R.id.menu_item_share:
ShareUtils.shareText(requireContext(), name, url,
currentInfo == null ? List.of() : currentInfo.getThumbnails());
break;
case R.id.menu_item_bookmark:
onBookmarkClicked();
break;
case R.id.menu_item_append_playlist:
if (currentInfo != null) {
disposables.add(PlaylistDialog.createCorrespondingDialog(
getContext(),
getPlayQueue()
.getStreams()
.stream()
.map(StreamEntity::new)
.collect(Collectors.toList()),
dialog -> dialog.show(getFM(), TAG)
));
}
break;
default:
return super.onOptionsItemSelected(item);
}
return true;
}
/*//////////////////////////////////////////////////////////////////////////
// Contract
//////////////////////////////////////////////////////////////////////////*/
@Override
public void showLoading() {
super.showLoading();
animate(headerBinding.getRoot(), false, 200);
animateHideRecyclerViewAllowingScrolling(itemsList);
CoilUtils.dispose(headerBinding.uploaderAvatarView);
animate(headerBinding.uploaderLayout, false, 200);
}
@Override
public void handleNextItems(final ListExtractor.InfoItemsPage result) {
super.handleNextItems(result);
setStreamCountAndOverallDuration(result.getItems(), !result.hasNextPage());
}
@Override
public void handleResult(@NonNull final PlaylistInfo result) {
super.handleResult(result);
animate(headerBinding.getRoot(), true, 100);
animate(headerBinding.uploaderLayout, true, 300);
headerBinding.uploaderLayout.setOnClickListener(null);
// If we have an uploader put them into the UI
if (!TextUtils.isEmpty(result.getUploaderName())) {
headerBinding.uploaderName.setText(result.getUploaderName());
if (!TextUtils.isEmpty(result.getUploaderUrl())) {
headerBinding.uploaderLayout.setOnClickListener(v -> {
try {
NavigationHelper.openChannelFragment(getFM(), result.getServiceId(),
result.getUploaderUrl(), result.getUploaderName());
} catch (final Exception e) {
ErrorUtil.showUiErrorSnackbar(this, "Opening channel fragment", e);
}
});
}
} else { // Otherwise say we have no uploader
headerBinding.uploaderName.setText(R.string.playlist_no_uploader);
}
playlistControlBinding.getRoot().setVisibility(View.VISIBLE);
if (result.getServiceId() == ServiceList.YouTube.getServiceId()
&& (YoutubeParsingHelper.isYoutubeMixId(result.getId())
|| YoutubeParsingHelper.isYoutubeMusicMixId(result.getId()))) {
// this is an auto-generated playlist (e.g. Youtube mix), so a radio is shown
final ShapeAppearanceModel model = ShapeAppearanceModel.builder()
.setAllCorners(CornerFamily.ROUNDED, 0f)
.build(); // this turns the image back into a square
headerBinding.uploaderAvatarView.setShapeAppearanceModel(model);
headerBinding.uploaderAvatarView.setStrokeColor(AppCompatResources
.getColorStateList(requireContext(), R.color.transparent_background_color));
headerBinding.uploaderAvatarView.setImageDrawable(
AppCompatResources.getDrawable(requireContext(),
R.drawable.ic_radio)
);
} else {
CoilHelper.INSTANCE.loadAvatar(headerBinding.uploaderAvatarView,
result.getUploaderAvatars());
}
streamCount = result.getStreamCount();
setStreamCountAndOverallDuration(result.getRelatedItems(), !result.hasNextPage());
final Description description = result.getDescription();
if (description != null && description != Description.EMPTY_DESCRIPTION
&& !isBlank(description.getContent())) {
final TextEllipsizer ellipsizer = new TextEllipsizer(
headerBinding.playlistDescription, 5, getServiceById(result.getServiceId()));
ellipsizer.setStateChangeListener(isEllipsized ->
headerBinding.playlistDescriptionReadMore.setText(
Boolean.TRUE.equals(isEllipsized) ? R.string.show_more : R.string.show_less
));
ellipsizer.setOnContentChanged(canBeEllipsized -> {
headerBinding.playlistDescriptionReadMore.setVisibility(
Boolean.TRUE.equals(canBeEllipsized) ? View.VISIBLE : View.GONE);
if (Boolean.TRUE.equals(canBeEllipsized)) {
ellipsizer.ellipsize();
}
});
ellipsizer.setContent(description);
headerBinding.playlistDescriptionReadMore.setOnClickListener(v -> ellipsizer.toggle());
headerBinding.playlistDescription.setOnClickListener(v -> ellipsizer.toggle());
} else {
headerBinding.playlistDescription.setVisibility(View.GONE);
headerBinding.playlistDescriptionReadMore.setVisibility(View.GONE);
}
if (!result.getErrors().isEmpty()) {
showSnackBarError(new ErrorInfo(result.getErrors(), UserAction.REQUESTED_PLAYLIST,
result.getUrl(), result));
}
remotePlaylistManager.getPlaylist(result)
.flatMap(lists -> getUpdateProcessor(lists, result), (lists, id) -> lists)
.onBackpressureLatest()
.observeOn(AndroidSchedulers.mainThread())
.subscribe(getPlaylistBookmarkSubscriber());
PlayButtonHelper.initPlaylistControlClickListener(activity, playlistControlBinding, this);
}
public PlayQueue getPlayQueue() {
return getPlayQueue(0);
}
private PlayQueue getPlayQueue(final int index) {
final List<StreamInfoItem> infoItems = new ArrayList<>();
for (final InfoItem i : infoListAdapter.getItemsList()) {
if (i instanceof StreamInfoItem) {
infoItems.add((StreamInfoItem) i);
class PlaylistFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?,
) = ComposeView(requireContext()).apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent {
AppTheme {
Playlist()
}
}
return new PlaylistPlayQueue(
currentInfo.getServiceId(),
currentInfo.getUrl(),
currentInfo.getNextPage(),
infoItems,
index
);
}
/*//////////////////////////////////////////////////////////////////////////
// Utils
//////////////////////////////////////////////////////////////////////////*/
private Flowable<Integer> getUpdateProcessor(
@NonNull final List<PlaylistRemoteEntity> playlists,
@NonNull final PlaylistInfo result) {
final Flowable<Integer> noItemToUpdate = Flowable.just(/*noItemToUpdate=*/-1);
if (playlists.isEmpty()) {
return noItemToUpdate;
}
final PlaylistRemoteEntity playlistRemoteEntity = playlists.get(0);
if (playlistRemoteEntity.isIdenticalTo(result)) {
return noItemToUpdate;
}
return remotePlaylistManager.onUpdate(playlists.get(0).getUid(), result).toFlowable();
}
private Subscriber<List<PlaylistRemoteEntity>> getPlaylistBookmarkSubscriber() {
return new Subscriber<>() {
@Override
public void onSubscribe(final Subscription s) {
if (bookmarkReactor != null) {
bookmarkReactor.cancel();
}
bookmarkReactor = s;
bookmarkReactor.request(1);
}
@Override
public void onNext(final List<PlaylistRemoteEntity> playlist) {
playlistEntity = playlist.isEmpty() ? null : playlist.get(0);
updateBookmarkButtons();
isBookmarkButtonReady.set(true);
if (bookmarkReactor != null) {
bookmarkReactor.request(1);
}
}
@Override
public void onError(final Throwable throwable) {
showError(new ErrorInfo(throwable, UserAction.REQUESTED_BOOKMARK,
"Get playlist bookmarks"));
}
@Override
public void onComplete() { }
};
}
@Override
public void setTitle(final String title) {
super.setTitle(title);
if (headerBinding != null) {
headerBinding.playlistTitleView.setText(title);
companion object {
@JvmStatic
fun getInstance(serviceId: Int, url: String?) = PlaylistFragment().apply {
arguments = bundleOf(KEY_SERVICE_ID to serviceId, KEY_URL to url)
}
}
private void onBookmarkClicked() {
if (isBookmarkButtonReady == null || !isBookmarkButtonReady.get()
|| remotePlaylistManager == null) {
return;
}
final Disposable action;
if (currentInfo != null && playlistEntity == null) {
action = remotePlaylistManager.onBookmark(currentInfo)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(ignored -> { /* Do nothing */ }, throwable ->
showError(new ErrorInfo(throwable, UserAction.REQUESTED_BOOKMARK,
"Adding playlist bookmark")));
} else if (playlistEntity != null) {
action = remotePlaylistManager.deletePlaylist(playlistEntity.getUid())
.observeOn(AndroidSchedulers.mainThread())
.doFinally(() -> playlistEntity = null)
.subscribe(ignored -> { /* Do nothing */ }, throwable ->
showError(new ErrorInfo(throwable, UserAction.REQUESTED_BOOKMARK,
"Deleting playlist bookmark")));
} else {
action = Disposable.empty();
}
disposables.add(action);
}
private void updateBookmarkButtons() {
if (playlistBookmarkButton == null || activity == null) {
return;
}
final int drawable = playlistEntity == null
? R.drawable.ic_playlist_add : R.drawable.ic_playlist_add_check;
final int titleRes = playlistEntity == null
? R.string.bookmark_playlist : R.string.unbookmark_playlist;
playlistBookmarkButton.setIcon(drawable);
playlistBookmarkButton.setTitle(titleRes);
}
private void setStreamCountAndOverallDuration(final List<StreamInfoItem> list,
final boolean isDurationComplete) {
if (activity != null && headerBinding != null) {
playlistOverallDurationSeconds += list.stream()
.mapToLong(StreamInfoItem::getDuration)
.sum();
headerBinding.playlistStreamCount.setText(
Localization.concatenateStrings(
Localization.localizeStreamCount(activity, streamCount),
Localization.getDurationString(playlistOverallDurationSeconds,
isDurationComplete, true))
);
}
}
}

View File

@ -1,25 +0,0 @@
package org.schabi.newpipe.fragments.list.playlist
import android.os.Bundle
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.fragment.app.Fragment
import org.schabi.newpipe.compose.playlist.Playlist
import org.schabi.newpipe.compose.theme.AppTheme
class PlaylistFragment2 : Fragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?,
) = ComposeView(requireContext()).apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent {
AppTheme {
Playlist()
}
}
}
}

View File

@ -140,7 +140,7 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
entry.name);
} else if (selectedItem instanceof PlaylistRemoteEntity entry) {
NavigationHelper.openPlaylistFragment(fragmentManager, entry.getServiceId(),
entry.getUrl(), entry.getName());
entry.getUrl());
}
}

View File

@ -577,9 +577,8 @@ public abstract class Tab {
public Fragment getFragment(final Context context) {
if (playlistType == LocalItemType.PLAYLIST_LOCAL_ITEM) {
return LocalPlaylistFragment.getInstance(playlistId, playlistName);
} else { // playlistType == LocalItemType.PLAYLIST_REMOTE_ITEM
return PlaylistFragment.getInstance(playlistServiceId, playlistUrl, playlistName);
return PlaylistFragment.getInstance(playlistServiceId, playlistUrl);
}
}
@ -606,12 +605,10 @@ public abstract class Tab {
@Override
public boolean equals(final Object obj) {
if (!(obj instanceof PlaylistTab)) {
if (!(obj instanceof PlaylistTab other)) {
return false;
}
final PlaylistTab other = (PlaylistTab) obj;
return super.equals(obj)
&& playlistServiceId == other.playlistServiceId // Remote
&& playlistId == other.playlistId // Local

View File

@ -47,7 +47,7 @@ import org.schabi.newpipe.fragments.MainFragment;
import org.schabi.newpipe.fragments.detail.VideoDetailFragment;
import org.schabi.newpipe.fragments.list.channel.ChannelFragment;
import org.schabi.newpipe.fragments.list.kiosk.KioskFragment;
import org.schabi.newpipe.fragments.list.playlist.PlaylistFragment2;
import org.schabi.newpipe.fragments.list.playlist.PlaylistFragment;
import org.schabi.newpipe.fragments.list.search.SearchFragment;
import org.schabi.newpipe.local.bookmark.BookmarkFragment;
import org.schabi.newpipe.local.feed.FeedFragment;
@ -502,14 +502,13 @@ public final class NavigationHelper {
}
public static void openPlaylistFragment(final FragmentManager fragmentManager,
final int serviceId, final String url,
@NonNull final String name) {
final int serviceId, final String url) {
final var args = new Bundle();
args.putInt(Constants.KEY_SERVICE_ID, serviceId);
args.putString(Constants.KEY_URL, url);
defaultTransaction(fragmentManager)
.replace(R.id.fragment_holder, PlaylistFragment2.class, args)
.replace(R.id.fragment_holder, PlaylistFragment.class, args)
.addToBackStack(null)
.commit();
}