From dfc27b2480ae8b267ca7882ad001230619cb4534 Mon Sep 17 00:00:00 2001 From: Roy Yosef Date: Thu, 30 Apr 2020 23:52:47 +0300 Subject: [PATCH] Add playlist tab to main page Add bookmarked playlist as tab in the main page (by Settings > Content > Content of main page) --- .../local/bookmark/BookmarkFragment.java | 30 +-- .../settings/SelectPlaylistFragment.java | 240 ++++++++++++++++++ .../settings/tabs/ChooseTabsFragment.java | 30 +++ .../org/schabi/newpipe/settings/tabs/Tab.java | 141 +++++++++- .../newpipe/util/PlaylistItemsUtils.java | 38 +++ .../res/layout/select_playlist_fragment.xml | 43 ++++ app/src/main/res/values/strings.xml | 3 + 7 files changed, 495 insertions(+), 30 deletions(-) create mode 100644 app/src/main/java/org/schabi/newpipe/settings/SelectPlaylistFragment.java create mode 100644 app/src/main/java/org/schabi/newpipe/util/PlaylistItemsUtils.java create mode 100644 app/src/main/res/layout/select_playlist_fragment.xml 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 d99a05976..7485a73ef 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 @@ -29,9 +29,8 @@ import org.schabi.newpipe.local.playlist.RemotePlaylistManager; import org.schabi.newpipe.report.UserAction; import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.OnClickGesture; +import org.schabi.newpipe.util.PlaylistItemsUtils; -import java.util.ArrayList; -import java.util.Collections; import java.util.List; import icepick.State; @@ -54,31 +53,6 @@ public final class BookmarkFragment extends BaseLocalListFragment merge( - final List localPlaylists, - final List remotePlaylists) { - List items = new ArrayList<>( - localPlaylists.size() + remotePlaylists.size()); - items.addAll(localPlaylists); - items.addAll(remotePlaylists); - - Collections.sort(items, (left, right) -> { - String on1 = left.getOrderingName(); - String on2 = right.getOrderingName(); - if (on1 == null && on2 == null) { - return 0; - } else if (on1 != null && on2 == null) { - return -1; - } else if (on1 == null && on2 != null) { - return 1; - } else { - return on1.compareToIgnoreCase(on2); - } - }); - - return items; - } - @Override public void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -164,7 +138,7 @@ public final class BookmarkFragment extends BaseLocalListFragment playlists = new Vector<>(); + + public void setOnSelectedLisener(final OnSelectedLisener listener) { + onSelectedLisener = listener; + } + + public void setOnCancelListener(final OnCancelListener listener) { + onCancelListener = listener; + } + + /*////////////////////////////////////////////////////////////////////////// + // Init + //////////////////////////////////////////////////////////////////////////*/ + + @Override + public View onCreateView(@NonNull final LayoutInflater inflater, final ViewGroup container, + final Bundle savedInstanceState) { + 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(this.getContext()); + LocalPlaylistManager localPlaylistManager = new LocalPlaylistManager(database); + RemotePlaylistManager remotePlaylistManager = new RemotePlaylistManager(database); + + Flowable.combineLatest(localPlaylistManager.getPlaylists(), + remotePlaylistManager.getPlaylists(), PlaylistItemsUtils::merge) + .toObservable() + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(getPlaylistsObserver()); + + return v; + } + + /*////////////////////////////////////////////////////////////////////////// + // Handle actions + //////////////////////////////////////////////////////////////////////////*/ + + @Override + public void onCancel(final DialogInterface dialogInterface) { + super.onCancel(dialogInterface); + if (onCancelListener != null) { + onCancelListener.onCancel(); + } + } + + private void clickedItem(final int position) { + if (onSelectedLisener != null) { + LocalItem selectedItem = playlists.get(position); + + if (selectedItem instanceof PlaylistMetadataEntry) { + final PlaylistMetadataEntry entry = ((PlaylistMetadataEntry) selectedItem); + onSelectedLisener + .onLocalPlaylistSelected(entry.uid, entry.name); + + } else if (selectedItem instanceof PlaylistRemoteEntity) { + final PlaylistRemoteEntity entry = ((PlaylistRemoteEntity) selectedItem); + onSelectedLisener.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); + + } + + private Observer> getPlaylistsObserver() { + return new Observer>() { + @Override + public void onSubscribe(final Disposable d) { } + + @Override + public void onNext(final List newPlaylists) { + displayPlaylists(newPlaylists); + } + + @Override + public void onError(final Throwable exception) { + SelectPlaylistFragment.this.onError(exception); + } + + @Override + public void onComplete() { } + }; + } + + /*////////////////////////////////////////////////////////////////////////// + // 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 OnSelectedLisener { + 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) { + 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) { + 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 6ebfbd73c..ad8ee043b 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.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.settings.tabs.AddTabDialog.ChooseTabListItem; import org.schabi.newpipe.util.ThemeHelper; @@ -211,6 +212,23 @@ public class ChooseTabsFragment extends Fragment { addTab(new Tab.ChannelTab(serviceId, url, name))); selectChannelFragment.show(requireFragmentManager(), "select_channel"); return; + case PLAYLIST: + SelectPlaylistFragment selectPlaylistFragment = new SelectPlaylistFragment(); + selectPlaylistFragment.setOnSelectedLisener( + new SelectPlaylistFragment.OnSelectedLisener() { + @Override + public void onLocalPlaylistSelected(final long id, final String name) { + addTab(new Tab.PlaylistTab(id, name)); + } + + @Override + public void onRemotePlaylistSelected( + final int serviceId, final String url, final String name) { + addTab(new Tab.PlaylistTab(serviceId, url, name)); + } + }); + selectPlaylistFragment.show(requireFragmentManager(), "select_playlist"); + return; default: addTab(type.getTab()); break; @@ -248,6 +266,11 @@ public class ChooseTabsFragment extends Fragment { R.attr.ic_kiosk_hot))); } break; + case PLAYLIST: + returnList.add(new ChooseTabListItem(tab.getTabId(), + getString(R.string.playlist_page_summary), + tab.getTabIconRes(context))); + break; default: if (!tabList.contains(tab)) { returnList.add(new ChooseTabListItem(context, tab)); @@ -393,6 +416,13 @@ public class ChooseTabsFragment extends Fragment { tabName = NewPipe.getNameOfService(((Tab.ChannelTab) tab) .getChannelServiceId()) + "/" + tab.getTabName(requireContext()); break; + case PLAYLIST: + int serviceId = ((Tab.PlaylistTab) tab).getPlaylistServiceId(); + String serviceName = serviceId == -1 + ? getString(R.string.local) + : NewPipe.getNameOfService(serviceId); + tabName = serviceName + "/" + tab.getTabName(requireContext()); + break; default: tabName = tab.getTabName(requireContext()); break; 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 d06b4b14e..28a4e2723 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 @@ -11,6 +11,7 @@ import com.grack.nanojson.JsonObject; import com.grack.nanojson.JsonSink; import org.schabi.newpipe.R; +import org.schabi.newpipe.database.LocalItem.LocalItemType; import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.StreamingService; import org.schabi.newpipe.extractor.exceptions.ExtractionException; @@ -18,9 +19,11 @@ import org.schabi.newpipe.fragments.BlankFragment; import org.schabi.newpipe.fragments.list.channel.ChannelFragment; import org.schabi.newpipe.fragments.list.kiosk.DefaultKioskFragment; import org.schabi.newpipe.fragments.list.kiosk.KioskFragment; +import org.schabi.newpipe.fragments.list.playlist.PlaylistFragment; import org.schabi.newpipe.local.bookmark.BookmarkFragment; import org.schabi.newpipe.local.feed.FeedFragment; import org.schabi.newpipe.local.history.StatisticsPlaylistFragment; +import org.schabi.newpipe.local.playlist.LocalPlaylistFragment; import org.schabi.newpipe.local.subscription.SubscriptionFragment; import org.schabi.newpipe.report.ErrorActivity; import org.schabi.newpipe.report.UserAction; @@ -33,7 +36,8 @@ import java.util.Objects; public abstract class Tab { private static final String JSON_TAB_ID_KEY = "tab_id"; - Tab() { } + Tab() { + } Tab(@NonNull final JsonObject jsonObject) { readDataFromJson(jsonObject); @@ -83,6 +87,8 @@ public abstract class Tab { return new KioskTab(jsonObject); case CHANNEL: return new ChannelTab(jsonObject); + case PLAYLIST: + return new PlaylistTab(jsonObject); } } @@ -147,7 +153,8 @@ public abstract class Tab { BOOKMARKS(new BookmarksTab()), HISTORY(new HistoryTab()), KIOSK(new KioskTab()), - CHANNEL(new ChannelTab()); + CHANNEL(new ChannelTab()), + PLAYLIST(new PlaylistTab()); private Tab tab; @@ -482,4 +489,134 @@ public abstract class Tab { return kioskId; } } + + public static class PlaylistTab extends Tab { + public static final int ID = 8; + private static final String JSON_PLAYLIST_SERVICE_ID_KEY = "playlist_service_id"; + private static final String JSON_PLAYLIST_URL_KEY = "playlist_url"; + private static final String JSON_PLAYLIST_NAME_KEY = "playlist_name"; + private static final String JSON_PLAYLIST_ID_KEY = "playlist_id"; + private static final String JSON_PLAYLIST_TYPE_KEY = "playlist_type"; + private int playlistServiceId; + private String playlistUrl; + private String playlistName; + private long playlistId; + private LocalItemType playlistType; + + private PlaylistTab() { + this.playlistName = ""; + this.playlistId = -1; + this.playlistType = LocalItemType.PLAYLIST_LOCAL_ITEM; + this.playlistServiceId = -1; + this.playlistUrl = ""; + } + + public PlaylistTab(final long playlistId, final String playlistName) { + this.playlistName = playlistName; + this.playlistId = playlistId; + this.playlistType = LocalItemType.PLAYLIST_LOCAL_ITEM; + this.playlistServiceId = -1; + this.playlistUrl = ""; + } + + public PlaylistTab(final int playlistServiceId, final String playlistUrl, + final String playlistName) { + this.playlistServiceId = playlistServiceId; + this.playlistUrl = playlistUrl; + this.playlistName = playlistName; + this.playlistType = LocalItemType.PLAYLIST_REMOTE_ITEM; + this.playlistId = -1; + } + + public PlaylistTab(final JsonObject jsonObject) { + super(jsonObject); + } + + @Override + public int getTabId() { + return ID; + } + + @Override + public String getTabName(final Context context) { + return playlistName; + } + + @DrawableRes + @Override + public int getTabIconRes(final Context context) { + return ThemeHelper.resolveResourceIdFromAttr(context, R.attr.ic_list); + } + + @Override + public Fragment getFragment(final Context context) { + if (playlistType == LocalItemType.PLAYLIST_LOCAL_ITEM) { + return LocalPlaylistFragment.getInstance(playlistId, + playlistName == null ? "" : playlistName); + + } else { // playlistType == LocalItemType.PLAYLIST_REMOTE_ITEM + return PlaylistFragment.getInstance(playlistServiceId, playlistUrl, + playlistName == null ? "" : playlistName); + } + } + + @Override + protected void writeDataToJson(final JsonSink writerSink) { + writerSink.value(JSON_PLAYLIST_SERVICE_ID_KEY, playlistServiceId) + .value(JSON_PLAYLIST_URL_KEY, playlistUrl) + .value(JSON_PLAYLIST_NAME_KEY, playlistName) + .value(JSON_PLAYLIST_ID_KEY, playlistId) + .value(JSON_PLAYLIST_TYPE_KEY, playlistType.toString()); + } + + @Override + protected void readDataFromJson(final JsonObject jsonObject) { + playlistServiceId = jsonObject.getInt(JSON_PLAYLIST_SERVICE_ID_KEY, -1); + playlistUrl = jsonObject.getString(JSON_PLAYLIST_URL_KEY, ""); + playlistName = jsonObject.getString(JSON_PLAYLIST_NAME_KEY, ""); + playlistId = jsonObject.getInt(JSON_PLAYLIST_ID_KEY, -1); + playlistType = LocalItemType.valueOf( + jsonObject.getString(JSON_PLAYLIST_TYPE_KEY, + LocalItemType.PLAYLIST_LOCAL_ITEM.toString()) + ); + } + + @Override + public boolean equals(final Object obj) { + boolean baseEqual = super.equals(obj) + && Objects.equals(playlistType, ((PlaylistTab) obj).playlistType) + && Objects.equals(playlistName, ((PlaylistTab) obj).playlistName); + + if (!baseEqual) { + return false; + } + + boolean localPlaylistEquals = playlistId == ((PlaylistTab) obj).playlistId; + boolean remotePlaylistEquals = + playlistServiceId == ((PlaylistTab) obj).playlistServiceId + && Objects.equals(playlistUrl, ((PlaylistTab) obj).playlistUrl); + + return localPlaylistEquals || remotePlaylistEquals; + } + + public int getPlaylistServiceId() { + return playlistServiceId; + } + + public String getPlaylistUrl() { + return playlistUrl; + } + + public String getPlaylistName() { + return playlistName; + } + + public long getPlaylistId() { + return playlistId; + } + + public LocalItemType getPlaylistType() { + return playlistType; + } + } } diff --git a/app/src/main/java/org/schabi/newpipe/util/PlaylistItemsUtils.java b/app/src/main/java/org/schabi/newpipe/util/PlaylistItemsUtils.java new file mode 100644 index 000000000..230be4d28 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/util/PlaylistItemsUtils.java @@ -0,0 +1,38 @@ +package org.schabi.newpipe.util; + +import org.schabi.newpipe.database.playlist.PlaylistLocalItem; +import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry; +import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public final class PlaylistItemsUtils { + private PlaylistItemsUtils() { } + + public static List merge( + final List localPlaylists, + final List remotePlaylists) { + List items = new ArrayList<>( + localPlaylists.size() + remotePlaylists.size()); + items.addAll(localPlaylists); + items.addAll(remotePlaylists); + + Collections.sort(items, (left, right) -> { + String on1 = left.getOrderingName(); + String on2 = right.getOrderingName(); + if (on1 == null && on2 == null) { + return 0; + } else if (on1 != null && on2 == null) { + return -1; + } else if (on1 == null && on2 != null) { + return 1; + } else { + return on1.compareToIgnoreCase(on2); + } + }); + + return items; + } +} diff --git a/app/src/main/res/layout/select_playlist_fragment.xml b/app/src/main/res/layout/select_playlist_fragment.xml new file mode 100644 index 000000000..14462662a --- /dev/null +++ b/app/src/main/res/layout/select_playlist_fragment.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a4dfd255d..79b86d2ad 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -399,6 +399,8 @@ Channel Page Select a channel No channel subscriptions yet + Select a playlist + No playlists bookmarks yet Select a kiosk Exported Imported @@ -651,4 +653,5 @@ Channel\'s avatar thumbnail Created by %s By %s + Playlist Page