diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java index 336e3997e27..fe0c66e587b 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java @@ -65,8 +65,8 @@ import org.schabi.newpipe.fragments.BaseStateFragment; import org.schabi.newpipe.fragments.EmptyFragment; import org.schabi.newpipe.fragments.list.comments.CommentsFragment; +import org.schabi.newpipe.fragments.list.playlist.AppendPlaylistDialog; import org.schabi.newpipe.fragments.list.videos.RelatedVideosFragment; -import org.schabi.newpipe.local.dialog.PlaylistAppendDialog; import org.schabi.newpipe.local.history.HistoryRecordManager; import org.schabi.newpipe.player.MainVideoPlayer; import org.schabi.newpipe.player.PopupVideoPlayer; @@ -409,7 +409,7 @@ public void onClick(final View v) { break; case R.id.detail_controls_playlist_append: if (getFragmentManager() != null && currentInfo != null) { - PlaylistAppendDialog.fromStreamInfo(currentInfo) + AppendPlaylistDialog.fromStreamInfo(currentInfo) .show(getFragmentManager(), TAG); } break; diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/AppendPlaylistDialog.java b/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/AppendPlaylistDialog.java new file mode 100644 index 00000000000..c4c982cad1e --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/AppendPlaylistDialog.java @@ -0,0 +1,115 @@ +package org.schabi.newpipe.fragments.list.playlist; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import org.schabi.newpipe.R; +import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry; +import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity; +import org.schabi.newpipe.database.stream.model.StreamEntity; +import org.schabi.newpipe.extractor.stream.StreamInfo; +import org.schabi.newpipe.extractor.stream.StreamInfoItem; +import org.schabi.newpipe.player.playqueue.PlayQueueItem; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +import io.reactivex.android.schedulers.AndroidSchedulers; + +public final class AppendPlaylistDialog extends PlaylistDialog + implements PlaylistDialog.OnSelectedListener { + public static final String TAG = AppendPlaylistDialog.class.getSimpleName(); + + public AppendPlaylistDialog(@Nullable final List streamEntities) { + super(streamEntities, false); + setOnSelectedListener(this); + } + + @Override + public View onCreateView(@NonNull final LayoutInflater inflater, + final ViewGroup container, + final Bundle savedInstanceState) { + + final View view = super.onCreateView(inflater, container, savedInstanceState); + + final View newPlaylistButton = Objects.requireNonNull(view).findViewById(R.id.newPlaylist); + newPlaylistButton.setVisibility(View.VISIBLE); + newPlaylistButton.setOnClickListener(v -> openCreatePlaylistDialog()); + + view.findViewById(R.id.titleTextView).setVisibility(View.GONE); + + return view; + } + + public static AppendPlaylistDialog fromStreamInfo(final StreamInfo streamInfo) { + return new AppendPlaylistDialog(Collections.singletonList(new StreamEntity(streamInfo))); + } + + public static AppendPlaylistDialog fromStreamInfoItems( + final List streamInfoItems) { + + final List streamEntities = new ArrayList<>(streamInfoItems.size()); + for (final StreamInfoItem streamInfoItem : streamInfoItems) { + streamEntities.add(new StreamEntity(streamInfoItem)); + } + return new AppendPlaylistDialog(streamEntities); + } + + public static AppendPlaylistDialog fromPlayQueueItems( + final List playQueueItems) { + + final List streamEntities = new ArrayList<>(playQueueItems.size()); + for (final PlayQueueItem playQueueItem : playQueueItems) { + streamEntities.add(new StreamEntity(playQueueItem)); + } + return new AppendPlaylistDialog(streamEntities); + } + + + @Override + public void onLocalPlaylistSelected(final PlaylistMetadataEntry localPlaylist) { + if (getStoredStreamEntities() != null) { + final Toast successToast = Toast.makeText(getContext(), + R.string.playlist_add_stream_success, Toast.LENGTH_SHORT); + + if (localPlaylist.thumbnailUrl + .equals("drawable://" + R.drawable.dummy_thumbnail_playlist)) { + dialogDisposables.add(getLocalPlaylistManager() + .changePlaylistThumbnail(localPlaylist.uid, + getStoredStreamEntities().get(0).getThumbnailUrl()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(ignored -> successToast.show())); + } + + dialogDisposables.add(getLocalPlaylistManager() + .appendToPlaylist(localPlaylist.uid, getStoredStreamEntities()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(ignored -> successToast.show())); + + requireDialog().dismiss(); + } + } + + @Override + public void onRemotePlaylistSelected(final PlaylistRemoteEntity remotePlaylist) { + // unreachable code + } + + public void openCreatePlaylistDialog() { + if (getStoredStreamEntities() == null) { + return; + } + + new CreatePlaylistDialog(getStoredStreamEntities()) + .show(getParentFragmentManager(), TAG); + requireDialog().dismiss(); + } +} diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/CreatePlaylistDialog.java b/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/CreatePlaylistDialog.java new file mode 100644 index 00000000000..969b533434a --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/CreatePlaylistDialog.java @@ -0,0 +1,132 @@ +package org.schabi.newpipe.fragments.list.playlist; + +import android.app.AlertDialog; +import android.app.Dialog; +import android.os.Bundle; +import android.view.View; +import android.view.Window; +import android.widget.EditText; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.DialogFragment; + +import org.schabi.newpipe.NewPipeDatabase; +import org.schabi.newpipe.R; +import org.schabi.newpipe.database.stream.model.StreamEntity; +import org.schabi.newpipe.local.playlist.LocalPlaylistManager; +import org.schabi.newpipe.util.StateSaver; + +import java.util.List; +import java.util.Queue; + +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.Disposable; + +public final class CreatePlaylistDialog extends DialogFragment implements StateSaver.WriteRead { + + /*////////////////////////////////////////////////////////////////////////// + // Dialog + //////////////////////////////////////////////////////////////////////////*/ + + private Disposable createPlaylistDisposable; + + private List streamEntities; + private StateSaver.SavedState savedState; + + CreatePlaylistDialog(final List streamEntities) { + this.streamEntities = streamEntities; + } + + protected List getStreams() { + return streamEntities; + } + + /*////////////////////////////////////////////////////////////////////////// + // LifeCycle + //////////////////////////////////////////////////////////////////////////*/ + + @Override + public void onCreate(@Nullable final Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + savedState = StateSaver.tryToRestore(savedInstanceState, this); + } + + @Override + public void onDestroy() { + super.onDestroy(); + StateSaver.onDestroy(savedState); + + if (createPlaylistDisposable != null) { + createPlaylistDisposable.dispose(); + } + } + + @NonNull + @Override + public Dialog onCreateDialog(@Nullable final Bundle savedInstanceState) { + if (getStreams() == null) { + final Dialog dialog = super.onCreateDialog(savedInstanceState); + //remove title + final Window window = dialog.getWindow(); + if (window != null) { + window.requestFeature(Window.FEATURE_NO_TITLE); + } + return dialog; + } + + View dialogView = View.inflate(getContext(), R.layout.dialog_playlist_name, null); + EditText nameInput = dialogView.findViewById(R.id.playlist_name); + + final AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(getContext()) + .setTitle(R.string.create_playlist) + .setView(dialogView) + .setCancelable(true) + .setNegativeButton(R.string.cancel, null) + .setPositiveButton(R.string.create, (dialogInterface, i) -> { + final String name = nameInput.getText().toString(); + final LocalPlaylistManager playlistManager = + new LocalPlaylistManager(NewPipeDatabase.getInstance(requireContext())); + final Toast successToast = Toast.makeText(getActivity(), + R.string.playlist_creation_success, + Toast.LENGTH_SHORT); + + createPlaylistDisposable = playlistManager.createPlaylist(name, getStreams()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(longs -> successToast.show()); + }); + + return dialogBuilder.create(); + } + + /*////////////////////////////////////////////////////////////////////////// + // State Saving + //////////////////////////////////////////////////////////////////////////*/ + + @Override + public String generateSuffix() { + final int size = streamEntities == null ? 0 : streamEntities.size(); + return "." + size + ".list"; + } + + @Override + public void writeTo(final Queue objectsToSave) { + objectsToSave.add(streamEntities); + } + + @Override + @SuppressWarnings("unchecked") + public void readFrom(@NonNull final Queue savedObjects) { + streamEntities = (List) savedObjects.poll(); + } + + @Override + public void onSaveInstanceState(@NonNull final Bundle outState) { + super.onSaveInstanceState(outState); + if (getActivity() != null) { + savedState = StateSaver.tryToSave(getActivity().isChangingConfigurations(), + savedState, outState, this); + } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistDialog.java b/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistDialog.java new file mode 100644 index 00000000000..e7ae8a252f0 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistDialog.java @@ -0,0 +1,307 @@ +package org.schabi.newpipe.fragments.list.playlist; + +import android.app.Activity; +import android.app.Dialog; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.Window; +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 com.nostra13.universalimageloader.core.DisplayImageOptions; +import com.nostra13.universalimageloader.core.ImageLoader; + +import org.schabi.newpipe.NewPipeDatabase; +import org.schabi.newpipe.R; +import org.schabi.newpipe.database.AppDatabase; +import org.schabi.newpipe.database.LocalItem; +import org.schabi.newpipe.database.playlist.PlaylistLocalItem; +import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry; +import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity; +import org.schabi.newpipe.database.stream.model.StreamEntity; +import org.schabi.newpipe.local.playlist.LocalPlaylistManager; +import org.schabi.newpipe.local.playlist.RemotePlaylistManager; +import org.schabi.newpipe.report.ErrorActivity; +import org.schabi.newpipe.report.UserAction; +import org.schabi.newpipe.util.Localization; +import org.schabi.newpipe.util.StateSaver; + +import java.util.ArrayList; +import java.util.List; +import java.util.Queue; + +import io.reactivex.Flowable; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.CompositeDisposable; + +public class PlaylistDialog extends DialogFragment implements StateSaver.WriteRead { + /** + * This contains the base display options for images. + */ + private static final DisplayImageOptions DISPLAY_IMAGE_OPTIONS + = new DisplayImageOptions.Builder().cacheInMemory(true).build(); + + + public interface OnSelectedListener { + void onLocalPlaylistSelected(PlaylistMetadataEntry localPlaylist); + void onRemotePlaylistSelected(PlaylistRemoteEntity remotePlaylist); + } + + + private ProgressBar progressBar; + private TextView emptyView; + private RecyclerView recyclerView; + + protected CompositeDisposable dialogDisposables = new CompositeDisposable(); + private StateSaver.SavedState savedState; + + @Nullable private OnSelectedListener onSelectedListener = null; + private LocalPlaylistManager localPlaylistManager; + + private List playlists = new ArrayList<>(); + @Nullable private List storedStreamEntities; + private final boolean showRemotePlaylists; + + + public PlaylistDialog(@Nullable final List storedStreamEntities, + final boolean showRemotePlaylists) { + this.storedStreamEntities = storedStreamEntities; + this.showRemotePlaylists = showRemotePlaylists; + } + + @Nullable + protected List getStoredStreamEntities() { + return storedStreamEntities; + } + + protected LocalPlaylistManager getLocalPlaylistManager() { + return localPlaylistManager; + } + + /*////////////////////////////////////////////////////////////////////////// + // Fragment's Lifecycle + //////////////////////////////////////////////////////////////////////////*/ + + @Override + public void onCreate(@Nullable final Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + savedState = StateSaver.tryToRestore(savedInstanceState, this); + } + + @Override + public View onCreateView(@NonNull final LayoutInflater inflater, final ViewGroup container, + final Bundle savedInstanceState) { + final View v = + inflater.inflate(R.layout.dialog_playlists, container, false); + recyclerView = v.findViewById(R.id.itemList); + recyclerView.setLayoutManager(new LinearLayoutManager(getContext())); + SelectPlaylistAdapter playlistAdapter = new SelectPlaylistAdapter(); + recyclerView.setAdapter(playlistAdapter); + + progressBar = v.findViewById(R.id.progressBar); + emptyView = v.findViewById(R.id.emptyStateView); + progressBar.setVisibility(View.VISIBLE); + recyclerView.setVisibility(View.GONE); + emptyView.setVisibility(View.GONE); + + final AppDatabase database = NewPipeDatabase.getInstance(requireContext()); + // save for usage in extending classes + localPlaylistManager = new LocalPlaylistManager(database); + + if (showRemotePlaylists) { + final RemotePlaylistManager remotePlaylistManager = new RemotePlaylistManager(database); + dialogDisposables.add(Flowable.combineLatest(localPlaylistManager.getPlaylists(), + remotePlaylistManager.getPlaylists(), PlaylistLocalItem::merge) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(this::displayPlaylists, this::onError)); + } else { + dialogDisposables.add(localPlaylistManager.getPlaylists() + .observeOn(AndroidSchedulers.mainThread()) + .subscribe((p) -> displayPlaylists(new ArrayList<>(p)), this::onError)); + } + + return v; + } + + @Override + public void onDestroy() { + super.onDestroy(); + StateSaver.onDestroy(savedState); + + if (dialogDisposables != null) { + dialogDisposables.dispose(); + dialogDisposables = null; + } + } + + @NonNull + @Override + public Dialog onCreateDialog(@Nullable final Bundle savedInstanceState) { + final Dialog dialog = super.onCreateDialog(savedInstanceState); + final Window window = dialog.getWindow(); + if (window != null) { // remove window title + window.requestFeature(Window.FEATURE_NO_TITLE); + } + return dialog; + } + + /*////////////////////////////////////////////////////////////////////////// + // State Saving + //////////////////////////////////////////////////////////////////////////*/ + + @Override + public String generateSuffix() { + final int size = storedStreamEntities == null ? 0 : storedStreamEntities.size(); + return "." + size + ".list"; + } + + @Override + public void writeTo(final Queue objectsToSave) { + objectsToSave.add(playlists); + objectsToSave.add(storedStreamEntities); + } + + @Override + @SuppressWarnings("unchecked") + public void readFrom(@NonNull final Queue savedObjects) { + playlists = (List) savedObjects.poll(); + storedStreamEntities = (List) savedObjects.poll(); + } + + @Override + public void onSaveInstanceState(@NonNull final Bundle outState) { + super.onSaveInstanceState(outState); + if (getActivity() != null) { + savedState = StateSaver.tryToSave(getActivity().isChangingConfigurations(), + savedState, outState, this); + } + } + + /*////////////////////////////////////////////////////////////////////////// + // Handle actions + //////////////////////////////////////////////////////////////////////////*/ + + public PlaylistDialog setOnSelectedListener(@Nullable final OnSelectedListener listener) { + onSelectedListener = listener; + return this; + } + + private void clickedItem(final int position) { + if (onSelectedListener != null) { + final LocalItem selectedItem = playlists.get(position); + + if (selectedItem instanceof PlaylistMetadataEntry) { + onSelectedListener.onLocalPlaylistSelected((PlaylistMetadataEntry) selectedItem); + + } else if (selectedItem instanceof PlaylistRemoteEntity) { + onSelectedListener.onRemotePlaylistSelected((PlaylistRemoteEntity) selectedItem); + } + } + dismiss(); + } + + /*////////////////////////////////////////////////////////////////////////// + // Item handling + //////////////////////////////////////////////////////////////////////////*/ + + private void displayPlaylists(final List newPlaylists) { + this.playlists = newPlaylists; + progressBar.setVisibility(View.GONE); + if (newPlaylists.isEmpty()) { + emptyView.setVisibility(View.VISIBLE); + return; + } + recyclerView.setVisibility(View.VISIBLE); + + } + + /*////////////////////////////////////////////////////////////////////////// + // Error + //////////////////////////////////////////////////////////////////////////*/ + + protected void onError(final Throwable e) { + final Activity activity = requireActivity(); + ErrorActivity.reportError(activity, e, activity.getClass(), null, ErrorActivity.ErrorInfo + .make(UserAction.UI_ERROR, "none", "", R.string.app_ui_crash)); + } + + /*////////////////////////////////////////////////////////////////////////// + // Adapter + //////////////////////////////////////////////////////////////////////////*/ + + private class SelectPlaylistAdapter + extends RecyclerView.Adapter { + @Override + @NonNull + public SelectPlaylistItemHolder onCreateViewHolder(final ViewGroup parent, + final int viewType) { + final View item = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.list_playlist_mini_item, parent, false); + return new SelectPlaylistItemHolder(item); + } + + @Override + public void onBindViewHolder(@NonNull final SelectPlaylistItemHolder holder, + final int position) { + final PlaylistLocalItem selectedItem = playlists.get(position); + + if (selectedItem instanceof PlaylistMetadataEntry) { + final PlaylistMetadataEntry entry = ((PlaylistMetadataEntry) selectedItem); + holder.updateItem(entry.name, "", entry.streamCount, entry.thumbnailUrl, + view -> clickedItem(position)); + + } else if (selectedItem instanceof PlaylistRemoteEntity) { + final PlaylistRemoteEntity entry = ((PlaylistRemoteEntity) selectedItem); + holder.updateItem(entry.getName(), entry.getUploader(), entry.getStreamCount(), + entry.getThumbnailUrl(), view -> clickedItem(position)); + } + } + + @Override + public int getItemCount() { + return playlists.size(); + } + + } + + public static class SelectPlaylistItemHolder extends RecyclerView.ViewHolder { + private final View view; + private final TextView titleView; + private final TextView uploaderView; + private final TextView streamCountView; + private final ImageView thumbnailView; + + SelectPlaylistItemHolder(final View view) { + super(view); + this.view = view; + titleView = view.findViewById(R.id.itemTitleView); + uploaderView = view.findViewById(R.id.itemUploaderView); + streamCountView = view.findViewById(R.id.itemStreamCountView); + thumbnailView = view.findViewById(R.id.itemThumbnailView); + } + + void updateItem(final String title, + final String uploader, + final long streamCount, + final String thumbnailUrl, + final View.OnClickListener onClickListener) { + + titleView.setText(title); + uploaderView.setText(uploader); + streamCountView.setText(Localization.localizeStreamCountMini( + streamCountView.getContext(), streamCount)); + ImageLoader.getInstance() + .displayImage(thumbnailUrl, thumbnailView, DISPLAY_IMAGE_OPTIONS); + view.setOnClickListener(onClickListener); + } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/local/dialog/PlaylistAppendDialog.java b/app/src/main/java/org/schabi/newpipe/local/dialog/PlaylistAppendDialog.java deleted file mode 100644 index 4eb97bbbf6f..00000000000 --- a/app/src/main/java/org/schabi/newpipe/local/dialog/PlaylistAppendDialog.java +++ /dev/null @@ -1,174 +0,0 @@ -package org.schabi.newpipe.local.dialog; - -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.Toast; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -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.LocalItem; -import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry; -import org.schabi.newpipe.database.stream.model.StreamEntity; -import org.schabi.newpipe.extractor.stream.StreamInfo; -import org.schabi.newpipe.extractor.stream.StreamInfoItem; -import org.schabi.newpipe.local.LocalItemListAdapter; -import org.schabi.newpipe.local.playlist.LocalPlaylistManager; -import org.schabi.newpipe.player.playqueue.PlayQueueItem; -import org.schabi.newpipe.util.OnClickGesture; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.disposables.CompositeDisposable; - -public final class PlaylistAppendDialog extends PlaylistDialog { - private static final String TAG = PlaylistAppendDialog.class.getCanonicalName(); - - private RecyclerView playlistRecyclerView; - private LocalItemListAdapter playlistAdapter; - - private CompositeDisposable playlistDisposables = new CompositeDisposable(); - - public static PlaylistAppendDialog fromStreamInfo(final StreamInfo info) { - PlaylistAppendDialog dialog = new PlaylistAppendDialog(); - dialog.setInfo(Collections.singletonList(new StreamEntity(info))); - return dialog; - } - - public static PlaylistAppendDialog fromStreamInfoItems(final List items) { - PlaylistAppendDialog dialog = new PlaylistAppendDialog(); - List entities = new ArrayList<>(items.size()); - for (final StreamInfoItem item : items) { - entities.add(new StreamEntity(item)); - } - dialog.setInfo(entities); - return dialog; - } - - public static PlaylistAppendDialog fromPlayQueueItems(final List items) { - PlaylistAppendDialog dialog = new PlaylistAppendDialog(); - List entities = new ArrayList<>(items.size()); - for (final PlayQueueItem item : items) { - entities.add(new StreamEntity(item)); - } - dialog.setInfo(entities); - return dialog; - } - - /*////////////////////////////////////////////////////////////////////////// - // LifeCycle - Creation - //////////////////////////////////////////////////////////////////////////*/ - - @Override - public View onCreateView(@NonNull final LayoutInflater inflater, final ViewGroup container, - final Bundle savedInstanceState) { - return inflater.inflate(R.layout.dialog_playlists, container); - } - - @Override - public void onViewCreated(@NonNull final View view, @Nullable final Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - - final LocalPlaylistManager playlistManager = - new LocalPlaylistManager(NewPipeDatabase.getInstance(getContext())); - - playlistAdapter = new LocalItemListAdapter(getActivity()); - playlistAdapter.setSelectedListener(new OnClickGesture() { - @Override - public void selected(final LocalItem selectedItem) { - if (!(selectedItem instanceof PlaylistMetadataEntry) || getStreams() == null) { - return; - } - onPlaylistSelected(playlistManager, (PlaylistMetadataEntry) selectedItem, - getStreams()); - } - }); - - playlistRecyclerView = view.findViewById(R.id.playlist_list); - playlistRecyclerView.setLayoutManager(new LinearLayoutManager(getContext())); - playlistRecyclerView.setAdapter(playlistAdapter); - - final View newPlaylistButton = view.findViewById(R.id.newPlaylist); - newPlaylistButton.setOnClickListener(ignored -> openCreatePlaylistDialog()); - - playlistDisposables.add(playlistManager.getPlaylists() - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(this::onPlaylistsReceived)); - } - - /*////////////////////////////////////////////////////////////////////////// - // LifeCycle - Destruction - //////////////////////////////////////////////////////////////////////////*/ - - @Override - public void onDestroyView() { - super.onDestroyView(); - playlistDisposables.dispose(); - if (playlistAdapter != null) { - playlistAdapter.unsetSelectedListener(); - } - - playlistDisposables.clear(); - playlistRecyclerView = null; - playlistAdapter = null; - } - - /*////////////////////////////////////////////////////////////////////////// - // Helper - //////////////////////////////////////////////////////////////////////////*/ - - public void openCreatePlaylistDialog() { - if (getStreams() == null || getFragmentManager() == null) { - return; - } - - PlaylistCreationDialog.newInstance(getStreams()).show(getFragmentManager(), TAG); - getDialog().dismiss(); - } - - private void onPlaylistsReceived(@NonNull final List playlists) { - if (playlists.isEmpty()) { - openCreatePlaylistDialog(); - return; - } - - if (playlistAdapter != null && playlistRecyclerView != null) { - playlistAdapter.clearStreamItemList(); - playlistAdapter.addItems(playlists); - playlistRecyclerView.setVisibility(View.VISIBLE); - } - } - - private void onPlaylistSelected(@NonNull final LocalPlaylistManager manager, - @NonNull final PlaylistMetadataEntry playlist, - @NonNull final List streams) { - if (getStreams() == null) { - return; - } - - final Toast successToast = Toast.makeText(getContext(), - R.string.playlist_add_stream_success, Toast.LENGTH_SHORT); - - if (playlist.thumbnailUrl.equals("drawable://" + R.drawable.dummy_thumbnail_playlist)) { - playlistDisposables.add(manager - .changePlaylistThumbnail(playlist.uid, streams.get(0).getThumbnailUrl()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(ignored -> successToast.show())); - } - - playlistDisposables.add(manager.appendToPlaylist(playlist.uid, streams) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(ignored -> successToast.show())); - - getDialog().dismiss(); - } -} diff --git a/app/src/main/java/org/schabi/newpipe/local/dialog/PlaylistCreationDialog.java b/app/src/main/java/org/schabi/newpipe/local/dialog/PlaylistCreationDialog.java deleted file mode 100644 index b25ec7288a5..00000000000 --- a/app/src/main/java/org/schabi/newpipe/local/dialog/PlaylistCreationDialog.java +++ /dev/null @@ -1,63 +0,0 @@ -package org.schabi.newpipe.local.dialog; - -import android.app.AlertDialog; -import android.app.Dialog; -import android.os.Bundle; -import android.view.View; -import android.widget.EditText; -import android.widget.Toast; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import org.schabi.newpipe.NewPipeDatabase; -import org.schabi.newpipe.R; -import org.schabi.newpipe.database.stream.model.StreamEntity; -import org.schabi.newpipe.local.playlist.LocalPlaylistManager; - -import java.util.List; - -import io.reactivex.android.schedulers.AndroidSchedulers; - -public final class PlaylistCreationDialog extends PlaylistDialog { - public static PlaylistCreationDialog newInstance(final List streams) { - PlaylistCreationDialog dialog = new PlaylistCreationDialog(); - dialog.setInfo(streams); - return dialog; - } - - /*////////////////////////////////////////////////////////////////////////// - // Dialog - //////////////////////////////////////////////////////////////////////////*/ - - @NonNull - @Override - public Dialog onCreateDialog(@Nullable final Bundle savedInstanceState) { - if (getStreams() == null) { - return super.onCreateDialog(savedInstanceState); - } - - View dialogView = View.inflate(getContext(), R.layout.dialog_playlist_name, null); - EditText nameInput = dialogView.findViewById(R.id.playlist_name); - - final AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(getContext()) - .setTitle(R.string.create_playlist) - .setView(dialogView) - .setCancelable(true) - .setNegativeButton(R.string.cancel, null) - .setPositiveButton(R.string.create, (dialogInterface, i) -> { - final String name = nameInput.getText().toString(); - final LocalPlaylistManager playlistManager = - new LocalPlaylistManager(NewPipeDatabase.getInstance(getContext())); - final Toast successToast = Toast.makeText(getActivity(), - R.string.playlist_creation_success, - Toast.LENGTH_SHORT); - - playlistManager.createPlaylist(name, getStreams()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(longs -> successToast.show()); - }); - - return dialogBuilder.create(); - } -} diff --git a/app/src/main/java/org/schabi/newpipe/local/dialog/PlaylistDialog.java b/app/src/main/java/org/schabi/newpipe/local/dialog/PlaylistDialog.java deleted file mode 100644 index 9ca8733ccd8..00000000000 --- a/app/src/main/java/org/schabi/newpipe/local/dialog/PlaylistDialog.java +++ /dev/null @@ -1,87 +0,0 @@ -package org.schabi.newpipe.local.dialog; - -import android.app.Dialog; -import android.os.Bundle; -import android.view.Window; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.fragment.app.DialogFragment; - -import org.schabi.newpipe.database.stream.model.StreamEntity; -import org.schabi.newpipe.util.StateSaver; - -import java.util.List; -import java.util.Queue; - -public abstract class PlaylistDialog extends DialogFragment implements StateSaver.WriteRead { - private List streamEntities; - - private StateSaver.SavedState savedState; - - protected void setInfo(final List entities) { - this.streamEntities = entities; - } - - protected List getStreams() { - return streamEntities; - } - - /*////////////////////////////////////////////////////////////////////////// - // LifeCycle - //////////////////////////////////////////////////////////////////////////*/ - - @Override - public void onCreate(@Nullable final Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - savedState = StateSaver.tryToRestore(savedInstanceState, this); - } - - @Override - public void onDestroy() { - super.onDestroy(); - StateSaver.onDestroy(savedState); - } - - @NonNull - @Override - public Dialog onCreateDialog(final Bundle savedInstanceState) { - final Dialog dialog = super.onCreateDialog(savedInstanceState); - //remove title - final Window window = dialog.getWindow(); - if (window != null) { - window.requestFeature(Window.FEATURE_NO_TITLE); - } - return dialog; - } - - /*////////////////////////////////////////////////////////////////////////// - // State Saving - //////////////////////////////////////////////////////////////////////////*/ - - @Override - public String generateSuffix() { - final int size = streamEntities == null ? 0 : streamEntities.size(); - return "." + size + ".list"; - } - - @Override - public void writeTo(final Queue objectsToSave) { - objectsToSave.add(streamEntities); - } - - @Override - @SuppressWarnings("unchecked") - public void readFrom(@NonNull final Queue savedObjects) { - streamEntities = (List) savedObjects.poll(); - } - - @Override - public void onSaveInstanceState(final Bundle outState) { - super.onSaveInstanceState(outState); - if (getActivity() != null) { - savedState = StateSaver.tryToSave(getActivity().isChangingConfigurations(), - savedState, outState, this); - } - } -} diff --git a/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java b/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java index 72becef8ff1..09591acf377 100644 --- a/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java +++ b/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java @@ -30,7 +30,7 @@ import org.schabi.newpipe.R; import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.fragments.OnScrollBelowItemsListener; -import org.schabi.newpipe.local.dialog.PlaylistAppendDialog; +import org.schabi.newpipe.fragments.list.playlist.AppendPlaylistDialog; import org.schabi.newpipe.player.event.PlayerEventListener; import org.schabi.newpipe.player.helper.PlaybackParameterDialog; import org.schabi.newpipe.player.playqueue.PlayQueueAdapter; @@ -551,7 +551,7 @@ private void appendAllToPlaylist() { } private void openPlaylistAppendDialog(final List playlist) { - PlaylistAppendDialog.fromPlayQueueItems(playlist) + AppendPlaylistDialog.fromPlayQueueItems(playlist) .show(getSupportFragmentManager(), getTag()); } diff --git a/app/src/main/java/org/schabi/newpipe/settings/SelectPlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/settings/SelectPlaylistFragment.java deleted file mode 100644 index 1d5c94421a7..00000000000 --- a/app/src/main/java/org/schabi/newpipe/settings/SelectPlaylistFragment.java +++ /dev/null @@ -1,225 +0,0 @@ -package org.schabi.newpipe.settings; - -import android.app.Activity; -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.fragment.app.DialogFragment; -import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; - -import com.nostra13.universalimageloader.core.DisplayImageOptions; -import com.nostra13.universalimageloader.core.ImageLoader; - -import org.schabi.newpipe.NewPipeDatabase; -import org.schabi.newpipe.R; -import org.schabi.newpipe.database.AppDatabase; -import org.schabi.newpipe.database.LocalItem; -import org.schabi.newpipe.database.playlist.PlaylistLocalItem; -import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry; -import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity; -import org.schabi.newpipe.local.playlist.LocalPlaylistManager; -import org.schabi.newpipe.local.playlist.RemotePlaylistManager; -import org.schabi.newpipe.report.ErrorActivity; -import org.schabi.newpipe.report.UserAction; - -import java.util.List; -import java.util.Vector; - -import io.reactivex.Flowable; -import io.reactivex.disposables.Disposable; - -public class SelectPlaylistFragment extends DialogFragment { - /** - * This contains the base display options for images. - */ - private static final DisplayImageOptions DISPLAY_IMAGE_OPTIONS - = new DisplayImageOptions.Builder().cacheInMemory(true).build(); - - private final ImageLoader imageLoader = ImageLoader.getInstance(); - - private OnSelectedListener onSelectedListener = null; - private OnCancelListener onCancelListener = null; - - private ProgressBar progressBar; - private TextView emptyView; - private RecyclerView recyclerView; - private Disposable playlistsSubscriber; - - private List playlists = new Vector<>(); - - public void setOnSelectedListener(final OnSelectedListener listener) { - onSelectedListener = listener; - } - - public void setOnCancelListener(final OnCancelListener listener) { - onCancelListener = listener; - } - - /*////////////////////////////////////////////////////////////////////////// - // Fragment's Lifecycle - //////////////////////////////////////////////////////////////////////////*/ - - @Override - public View onCreateView(@NonNull final LayoutInflater inflater, final ViewGroup container, - final Bundle savedInstanceState) { - final View v = - inflater.inflate(R.layout.select_playlist_fragment, container, false); - recyclerView = v.findViewById(R.id.items_list); - recyclerView.setLayoutManager(new LinearLayoutManager(getContext())); - SelectPlaylistAdapter playlistAdapter = new SelectPlaylistAdapter(); - recyclerView.setAdapter(playlistAdapter); - - 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()); - final LocalPlaylistManager localPlaylistManager = new LocalPlaylistManager(database); - final RemotePlaylistManager remotePlaylistManager = new RemotePlaylistManager(database); - - playlistsSubscriber = Flowable.combineLatest(localPlaylistManager.getPlaylists(), - remotePlaylistManager.getPlaylists(), PlaylistLocalItem::merge) - .subscribe(this::displayPlaylists, this::onError); - - return v; - } - - @Override - public void onDestroy() { - super.onDestroy(); - - if (playlistsSubscriber != null) { - playlistsSubscriber.dispose(); - playlistsSubscriber = null; - } - } - - /*////////////////////////////////////////////////////////////////////////// - // Handle actions - //////////////////////////////////////////////////////////////////////////*/ - - @Override - public void onCancel(final DialogInterface dialogInterface) { - super.onCancel(dialogInterface); - if (onCancelListener != null) { - onCancelListener.onCancel(); - } - } - - private void clickedItem(final int position) { - if (onSelectedListener != null) { - final LocalItem selectedItem = playlists.get(position); - - if (selectedItem instanceof PlaylistMetadataEntry) { - final PlaylistMetadataEntry entry = ((PlaylistMetadataEntry) selectedItem); - onSelectedListener - .onLocalPlaylistSelected(entry.uid, entry.name); - - } else if (selectedItem instanceof PlaylistRemoteEntity) { - final PlaylistRemoteEntity entry = ((PlaylistRemoteEntity) selectedItem); - onSelectedListener.onRemotePlaylistSelected( - entry.getServiceId(), entry.getUrl(), entry.getName()); - } - } - dismiss(); - } - - /*////////////////////////////////////////////////////////////////////////// - // Item handling - //////////////////////////////////////////////////////////////////////////*/ - - private void displayPlaylists(final List newPlaylists) { - this.playlists = newPlaylists; - progressBar.setVisibility(View.GONE); - if (newPlaylists.isEmpty()) { - emptyView.setVisibility(View.VISIBLE); - return; - } - recyclerView.setVisibility(View.VISIBLE); - - } - - /*////////////////////////////////////////////////////////////////////////// - // Error - //////////////////////////////////////////////////////////////////////////*/ - - protected void onError(final Throwable e) { - final Activity activity = getActivity(); - ErrorActivity.reportError(activity, e, activity.getClass(), null, ErrorActivity.ErrorInfo - .make(UserAction.UI_ERROR, "none", "", R.string.app_ui_crash)); - } - - /*////////////////////////////////////////////////////////////////////////// - // Interfaces - //////////////////////////////////////////////////////////////////////////*/ - - public interface OnSelectedListener { - void onLocalPlaylistSelected(long id, String name); - void onRemotePlaylistSelected(int serviceId, String url, String name); - } - - public interface OnCancelListener { - void onCancel(); - } - - private class SelectPlaylistAdapter - extends RecyclerView.Adapter { - @Override - public SelectPlaylistItemHolder onCreateViewHolder(final ViewGroup parent, - final int viewType) { - final View item = LayoutInflater.from(parent.getContext()) - .inflate(R.layout.list_playlist_mini_item, parent, false); - return new SelectPlaylistItemHolder(item); - } - - @Override - public void onBindViewHolder(final SelectPlaylistItemHolder holder, final int position) { - final PlaylistLocalItem selectedItem = playlists.get(position); - - if (selectedItem instanceof PlaylistMetadataEntry) { - final PlaylistMetadataEntry entry = ((PlaylistMetadataEntry) selectedItem); - - holder.titleView.setText(entry.name); - holder.view.setOnClickListener(view -> clickedItem(position)); - imageLoader.displayImage(entry.thumbnailUrl, holder.thumbnailView, - DISPLAY_IMAGE_OPTIONS); - - } else if (selectedItem instanceof PlaylistRemoteEntity) { - final PlaylistRemoteEntity entry = ((PlaylistRemoteEntity) selectedItem); - - holder.titleView.setText(entry.getName()); - holder.view.setOnClickListener(view -> clickedItem(position)); - imageLoader.displayImage(entry.getThumbnailUrl(), holder.thumbnailView, - DISPLAY_IMAGE_OPTIONS); - } - } - - @Override - public int getItemCount() { - return playlists.size(); - } - - public class SelectPlaylistItemHolder extends RecyclerView.ViewHolder { - public final View view; - final ImageView thumbnailView; - final TextView titleView; - - SelectPlaylistItemHolder(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 1b26cd529e9..621443cc6f5 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 @@ -29,12 +29,14 @@ import com.google.android.material.floatingactionbutton.FloatingActionButton; import org.schabi.newpipe.R; +import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry; +import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity; import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.report.ErrorActivity; import org.schabi.newpipe.report.UserAction; import org.schabi.newpipe.settings.SelectChannelFragment; import org.schabi.newpipe.settings.SelectKioskFragment; -import org.schabi.newpipe.settings.SelectPlaylistFragment; +import org.schabi.newpipe.fragments.list.playlist.PlaylistDialog; import org.schabi.newpipe.settings.tabs.AddTabDialog.ChooseTabListItem; import org.schabi.newpipe.util.ThemeHelper; @@ -213,21 +215,22 @@ private void addTab(final int tabId) { selectChannelFragment.show(requireFragmentManager(), "select_channel"); return; case PLAYLIST: - SelectPlaylistFragment selectPlaylistFragment = new SelectPlaylistFragment(); - selectPlaylistFragment.setOnSelectedListener( - new SelectPlaylistFragment.OnSelectedListener() { + new PlaylistDialog(null, true) + .setOnSelectedListener(new PlaylistDialog.OnSelectedListener() { @Override - public void onLocalPlaylistSelected(final long id, final String name) { - addTab(new Tab.PlaylistTab(id, name)); + public void onLocalPlaylistSelected( + final PlaylistMetadataEntry localPlaylist) { + addTab(new Tab.PlaylistTab(localPlaylist.uid, localPlaylist.name)); } @Override public void onRemotePlaylistSelected( - final int serviceId, final String url, final String name) { - addTab(new Tab.PlaylistTab(serviceId, url, name)); + final PlaylistRemoteEntity remotePlaylist) { + addTab(new Tab.PlaylistTab(remotePlaylist.getServiceId(), + remotePlaylist.getUrl(), remotePlaylist.getName())); } - }); - selectPlaylistFragment.show(requireFragmentManager(), "select_playlist"); + }) + .show(getParentFragmentManager(), "select_playlist"); return; default: addTab(type.getTab()); diff --git a/app/src/main/java/org/schabi/newpipe/util/StreamDialogEntry.java b/app/src/main/java/org/schabi/newpipe/util/StreamDialogEntry.java index 92aee8ba704..f379d3f3ae6 100644 --- a/app/src/main/java/org/schabi/newpipe/util/StreamDialogEntry.java +++ b/app/src/main/java/org/schabi/newpipe/util/StreamDialogEntry.java @@ -6,7 +6,7 @@ import org.schabi.newpipe.R; import org.schabi.newpipe.extractor.stream.StreamInfoItem; -import org.schabi.newpipe.local.dialog.PlaylistAppendDialog; +import org.schabi.newpipe.fragments.list.playlist.AppendPlaylistDialog; import org.schabi.newpipe.player.playqueue.SinglePlayQueue; import java.util.Collections; @@ -39,14 +39,13 @@ public enum StreamDialogEntry { }), // has to be set manually append_playlist(R.string.append_playlist, (fragment, item) -> { - if (fragment.getFragmentManager() != null) { - PlaylistAppendDialog.fromStreamInfoItems(Collections.singletonList(item)) - .show(fragment.getFragmentManager(), "StreamDialogEntry@append_playlist"); - } + AppendPlaylistDialog.fromStreamInfoItems(Collections.singletonList(item)) + .show(fragment.getParentFragmentManager(), + "StreamDialogEntry@append_playlist"); }), share(R.string.share, (fragment, item) -> - ShareUtils.shareUrl(fragment.getContext(), item.getName(), item.getUrl())); + ShareUtils.shareUrl(fragment.requireContext(), item.getName(), item.getUrl())); /////////////// diff --git a/app/src/main/res/layout/dialog_playlists.xml b/app/src/main/res/layout/dialog_playlists.xml index eebeb29515b..f9157c4a0f7 100644 --- a/app/src/main/res/layout/dialog_playlists.xml +++ b/app/src/main/res/layout/dialog_playlists.xml @@ -1,16 +1,30 @@ - + android:layout_height="match_parent"> + + + android:clickable="true" + android:visibility="gone" + tools:visibility="visible"> + - - + + + + + + diff --git a/app/src/main/res/layout/select_playlist_fragment.xml b/app/src/main/res/layout/select_playlist_fragment.xml deleted file mode 100644 index ca0d49e3258..00000000000 --- a/app/src/main/res/layout/select_playlist_fragment.xml +++ /dev/null @@ -1,45 +0,0 @@ - - - - - - - - - - - - - -