diff --git a/app/src/main/java/org/schabi/newpipe/MainActivity.java b/app/src/main/java/org/schabi/newpipe/MainActivity.java index 6b95ea8a8..d05acb057 100644 --- a/app/src/main/java/org/schabi/newpipe/MainActivity.java +++ b/app/src/main/java/org/schabi/newpipe/MainActivity.java @@ -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)) { diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java index 8a117a47a..d433fccf6 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java @@ -279,7 +279,7 @@ public abstract class BaseListFragment extends BaseStateFragment 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); } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.kt b/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.kt index 74ce5c627..4f030be34 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.kt +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.kt @@ -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 - 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 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> loadMoreItemsLogic() { - return ExtractorHelper.getMorePlaylistItems(serviceId, url, currentNextPage); - } - - @Override - protected Single 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 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 getUpdateProcessor( - @NonNull final List playlists, - @NonNull final PlaylistInfo result) { - final Flowable 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> 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 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 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)) - ); - } - } - } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment2.kt b/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment2.kt deleted file mode 100644 index 1106c9b40..000000000 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment2.kt +++ /dev/null @@ -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() - } - } - } -} diff --git a/app/src/main/java/org/schabi/newpipe/local/bookmark/BookmarkFragment.java b/app/src/main/java/org/schabi/newpipe/local/bookmark/BookmarkFragment.java index e21e14500..a8fe19dd4 100644 --- a/app/src/main/java/org/schabi/newpipe/local/bookmark/BookmarkFragment.java +++ b/app/src/main/java/org/schabi/newpipe/local/bookmark/BookmarkFragment.java @@ -140,7 +140,7 @@ public final class BookmarkFragment extends BaseLocalListFragment