-Added ability to save playlist as remote playlist link rather than storing it in database.

-Added LeakCanary as part of debug build.
-Modified bookmark list to show both remote and local playlists.
-Removed ability to save channel items as local playlist, in favor of subscribe.
This commit is contained in:
John Zhen Mo 2018-02-05 21:32:23 -08:00
parent efd4db40ef
commit c0a75f5b98
35 changed files with 625 additions and 183 deletions

View File

@ -89,4 +89,6 @@ dependencies {
implementation 'frankiesardo:icepick:3.2.0'
annotationProcessor 'frankiesardo:icepick-processor:3.2.0'
debugImplementation 'com.squareup.leakcanary:leakcanary-android:1.5.4'
}

View File

@ -4,6 +4,7 @@ import android.content.Context;
import android.support.multidex.MultiDex;
import com.facebook.stetho.Stetho;
import com.squareup.leakcanary.LeakCanary;
public class DebugApp extends App {
private static final String TAG = DebugApp.class.toString();
@ -18,6 +19,13 @@ public class DebugApp extends App {
public void onCreate() {
super.onCreate();
if (LeakCanary.isInAnalyzerProcess(this)) {
// This process is dedicated to LeakCanary for heap analysis.
// You should not init your app in this process.
return;
}
LeakCanary.install(this);
initStetho();
}

View File

@ -1,12 +1,9 @@
package org.schabi.newpipe;
import android.app.AlarmManager;
import android.app.Application;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.util.Log;

View File

@ -9,8 +9,10 @@ import org.schabi.newpipe.database.history.dao.StreamHistoryDAO;
import org.schabi.newpipe.database.history.model.SearchHistoryEntry;
import org.schabi.newpipe.database.history.model.StreamHistoryEntity;
import org.schabi.newpipe.database.playlist.dao.PlaylistDAO;
import org.schabi.newpipe.database.playlist.dao.PlaylistRemoteDAO;
import org.schabi.newpipe.database.playlist.dao.PlaylistStreamDAO;
import org.schabi.newpipe.database.playlist.model.PlaylistEntity;
import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity;
import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity;
import org.schabi.newpipe.database.stream.dao.StreamDAO;
import org.schabi.newpipe.database.stream.dao.StreamStateDAO;
@ -26,7 +28,7 @@ import static org.schabi.newpipe.database.Migrations.DB_VER_12_0;
entities = {
SubscriptionEntity.class, SearchHistoryEntry.class,
StreamEntity.class, StreamHistoryEntity.class, StreamStateEntity.class,
PlaylistEntity.class, PlaylistStreamEntity.class
PlaylistEntity.class, PlaylistStreamEntity.class, PlaylistRemoteEntity.class
},
version = DB_VER_12_0,
exportSchema = false
@ -48,4 +50,6 @@ public abstract class AppDatabase extends RoomDatabase {
public abstract PlaylistDAO playlistDAO();
public abstract PlaylistStreamDAO playlistStreamDAO();
public abstract PlaylistRemoteDAO playlistRemoteDAO();
}

View File

@ -2,9 +2,11 @@ package org.schabi.newpipe.database;
public interface LocalItem {
enum LocalItemType {
PLAYLIST_ITEM,
PLAYLIST_LOCAL_ITEM,
PLAYLIST_REMOTE_ITEM,
PLAYLIST_STREAM_ITEM,
STATISTIC_STREAM_ITEM
STATISTIC_STREAM_ITEM,
}
LocalItemType getLocalItemType();

View File

@ -0,0 +1,7 @@
package org.schabi.newpipe.database.playlist;
import org.schabi.newpipe.database.LocalItem;
public interface PlaylistLocalItem extends LocalItem {
String getOrderingName();
}

View File

@ -2,13 +2,11 @@ package org.schabi.newpipe.database.playlist;
import android.arch.persistence.room.ColumnInfo;
import org.schabi.newpipe.database.LocalItem;
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_ID;
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_NAME;
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_THUMBNAIL_URL;
public class PlaylistMetadataEntry implements LocalItem {
public class PlaylistMetadataEntry implements PlaylistLocalItem {
final public static String PLAYLIST_STREAM_COUNT = "streamCount";
@ColumnInfo(name = PLAYLIST_ID)
@ -29,6 +27,11 @@ public class PlaylistMetadataEntry implements LocalItem {
@Override
public LocalItemType getLocalItemType() {
return LocalItemType.PLAYLIST_ITEM;
return LocalItemType.PLAYLIST_LOCAL_ITEM;
}
@Override
public String getOrderingName() {
return name;
}
}

View File

@ -0,0 +1,60 @@
package org.schabi.newpipe.database.playlist.dao;
import android.arch.persistence.room.Dao;
import android.arch.persistence.room.Query;
import android.arch.persistence.room.Transaction;
import org.schabi.newpipe.database.BasicDAO;
import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity;
import java.util.List;
import io.reactivex.Flowable;
import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_ID;
import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_SERVICE_ID;
import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_TABLE;
import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_URL;
@Dao
public abstract class PlaylistRemoteDAO implements BasicDAO<PlaylistRemoteEntity> {
@Override
@Query("SELECT * FROM " + REMOTE_PLAYLIST_TABLE)
public abstract Flowable<List<PlaylistRemoteEntity>> getAll();
@Override
@Query("DELETE FROM " + REMOTE_PLAYLIST_TABLE)
public abstract int deleteAll();
@Override
@Query("SELECT * FROM " + REMOTE_PLAYLIST_TABLE +
" WHERE " + REMOTE_PLAYLIST_SERVICE_ID + " = :serviceId")
public abstract Flowable<List<PlaylistRemoteEntity>> listByService(int serviceId);
@Query("SELECT * FROM " + REMOTE_PLAYLIST_TABLE + " WHERE " +
REMOTE_PLAYLIST_URL + " = :url AND " +
REMOTE_PLAYLIST_SERVICE_ID + " = :serviceId")
public abstract Flowable<List<PlaylistRemoteEntity>> getPlaylist(long serviceId, String url);
@Query("SELECT " + REMOTE_PLAYLIST_ID + " FROM " + REMOTE_PLAYLIST_TABLE +
" WHERE " +
REMOTE_PLAYLIST_URL + " = :url AND " + REMOTE_PLAYLIST_SERVICE_ID + " = :serviceId")
abstract Long getPlaylistIdInternal(long serviceId, String url);
@Transaction
public long upsert(PlaylistRemoteEntity playlist) {
final Long playlistId = getPlaylistIdInternal(playlist.getServiceId(), playlist.getUrl());
if (playlistId == null) {
return insert(playlist);
} else {
playlist.setUid(playlistId);
update(playlist);
return playlistId;
}
}
@Query("DELETE FROM " + REMOTE_PLAYLIST_TABLE +
" WHERE " + REMOTE_PLAYLIST_ID + " = :playlistId")
public abstract int deletePlaylist(final long playlistId);
}

View File

@ -0,0 +1,138 @@
package org.schabi.newpipe.database.playlist.model;
import android.arch.persistence.room.ColumnInfo;
import android.arch.persistence.room.Entity;
import android.arch.persistence.room.Ignore;
import android.arch.persistence.room.Index;
import android.arch.persistence.room.PrimaryKey;
import org.schabi.newpipe.database.LocalItem;
import org.schabi.newpipe.database.playlist.PlaylistLocalItem;
import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
import org.schabi.newpipe.util.Constants;
import static org.schabi.newpipe.database.LocalItem.LocalItemType.PLAYLIST_REMOTE_ITEM;
import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_NAME;
import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_SERVICE_ID;
import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_TABLE;
import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_URL;
@Entity(tableName = REMOTE_PLAYLIST_TABLE,
indices = {
@Index(value = {REMOTE_PLAYLIST_NAME}),
@Index(value = {REMOTE_PLAYLIST_SERVICE_ID, REMOTE_PLAYLIST_URL}, unique = true)
})
public class PlaylistRemoteEntity implements PlaylistLocalItem {
final public static String REMOTE_PLAYLIST_TABLE = "remote_playlists";
final public static String REMOTE_PLAYLIST_ID = "uid";
final public static String REMOTE_PLAYLIST_SERVICE_ID = "service_id";
final public static String REMOTE_PLAYLIST_NAME = "name";
final public static String REMOTE_PLAYLIST_URL = "url";
final public static String REMOTE_PLAYLIST_THUMBNAIL_URL = "thumbnail_url";
final public static String REMOTE_PLAYLIST_UPLOADER_NAME = "uploader";
final public static String REMOTE_PLAYLIST_STREAM_COUNT = "stream_count";
@PrimaryKey(autoGenerate = true)
@ColumnInfo(name = REMOTE_PLAYLIST_ID)
private long uid = 0;
@ColumnInfo(name = REMOTE_PLAYLIST_SERVICE_ID)
private int serviceId = Constants.NO_SERVICE_ID;
@ColumnInfo(name = REMOTE_PLAYLIST_NAME)
private String name;
@ColumnInfo(name = REMOTE_PLAYLIST_URL)
private String url;
@ColumnInfo(name = REMOTE_PLAYLIST_THUMBNAIL_URL)
private String thumbnailUrl;
@ColumnInfo(name = REMOTE_PLAYLIST_UPLOADER_NAME)
private String uploader;
@ColumnInfo(name = REMOTE_PLAYLIST_STREAM_COUNT)
private Long streamCount;
public PlaylistRemoteEntity(int serviceId, String name, String url, String thumbnailUrl,
String uploader, Long streamCount) {
this.serviceId = serviceId;
this.name = name;
this.url = url;
this.thumbnailUrl = thumbnailUrl;
this.uploader = uploader;
this.streamCount = streamCount;
}
@Ignore
public PlaylistRemoteEntity(final PlaylistInfo info) {
this(info.getServiceId(), info.getName(), info.getUrl(), info.getThumbnailUrl(),
info.getUploaderName(), info.getStreamCount());
}
public long getUid() {
return uid;
}
public void setUid(long uid) {
this.uid = uid;
}
public int getServiceId() {
return serviceId;
}
public void setServiceId(int serviceId) {
this.serviceId = serviceId;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getThumbnailUrl() {
return thumbnailUrl;
}
public void setThumbnailUrl(String thumbnailUrl) {
this.thumbnailUrl = thumbnailUrl;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public String getUploader() {
return uploader;
}
public void setUploader(String uploader) {
this.uploader = uploader;
}
public Long getStreamCount() {
return streamCount;
}
public void setStreamCount(Long streamCount) {
this.streamCount = streamCount;
}
@Override
public LocalItemType getLocalItemType() {
return PLAYLIST_REMOTE_ITEM;
}
@Override
public String getOrderingName() {
return name;
}
}

View File

@ -290,20 +290,4 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> implem
public void handleNextItems(N result) {
isLoading.set(false);
}
/*//////////////////////////////////////////////////////////////////////////
// Utils
//////////////////////////////////////////////////////////////////////////*/
protected void appendToPlaylist(final android.support.v4.app.FragmentManager manager,
final String tag) {
if (infoListAdapter == null) return;
List<StreamInfoItem> streams = new ArrayList<>();
for (final InfoItem item : infoListAdapter.getItemsList()) {
if (item instanceof StreamInfoItem) {
streams.add((StreamInfoItem) item);
}
}
PlaylistAppendDialog.fromStreamInfoItems(streams).show(manager, tag);
}
}

View File

@ -84,7 +84,6 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
private LinearLayout headerBackgroundButton;
private MenuItem menuRssButton;
private MenuItem playlistAppendButton;
public static ChannelFragment getInstance(int serviceId, String url, String name) {
ChannelFragment instance = new ChannelFragment();
@ -203,12 +202,6 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
if (DEBUG) Log.d(TAG, "onCreateOptionsMenu() called with: menu = [" + menu +
"], inflater = [" + inflater + "]");
menuRssButton = menu.findItem(R.id.menu_item_rss);
playlistAppendButton = menu.findItem(R.id.menu_append_playlist);
if (currentInfo != null) {
menuRssButton.setVisible(!TextUtils.isEmpty(currentInfo.getFeedUrl()));
playlistAppendButton.setVisible(!currentInfo.getRelatedStreams().isEmpty());
}
}
}
@ -232,9 +225,6 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
case R.id.menu_item_share:
shareUrl(name, url);
break;
case R.id.menu_append_playlist:
appendToPlaylist(getFragmentManager(), TAG);
break;
default:
return super.onOptionsItemSelected(item);
}
@ -434,8 +424,6 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
} else headerSubscribersTextView.setVisibility(View.GONE);
if (menuRssButton != null) menuRssButton.setVisible(!TextUtils.isEmpty(result.getFeedUrl()));
if (playlistAppendButton != null) playlistAppendButton
.setVisible(!currentInfo.getRelatedStreams().isEmpty());
playlistCtrl.setVisibility(View.VISIBLE);

View File

@ -17,13 +17,18 @@ import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
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.extractor.ListExtractor;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
import org.schabi.newpipe.fragments.local.RemotePlaylistManager;
import org.schabi.newpipe.info_list.InfoItemDialog;
import org.schabi.newpipe.playlist.PlayQueue;
import org.schabi.newpipe.playlist.PlaylistPlayQueue;
@ -32,12 +37,21 @@ import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.ExtractorHelper;
import org.schabi.newpipe.util.NavigationHelper;
import java.util.List;
import io.reactivex.Single;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable;
import static org.schabi.newpipe.util.AnimationUtils.animateView;
public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
private CompositeDisposable disposables;
private Subscription bookmarkReactor;
private RemotePlaylistManager remotePlaylistManager;
private PlaylistRemoteEntity playlistEntity;
/*//////////////////////////////////////////////////////////////////////////
// Views
//////////////////////////////////////////////////////////////////////////*/
@ -54,7 +68,8 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
private View headerPopupButton;
private View headerBackgroundButton;
private MenuItem playlistAppendButton;
private MenuItem playlistBookmarkButton;
private MenuItem playlistUnbookmarkButton;
public static PlaylistFragment getInstance(int serviceId, String url, String name) {
PlaylistFragment instance = new PlaylistFragment();
@ -67,7 +82,15 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
//////////////////////////////////////////////////////////////////////////*/
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
disposables = new CompositeDisposable();
remotePlaylistManager = new RemotePlaylistManager(NewPipeDatabase.getInstance(getContext()));
}
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_playlist, container, false);
}
@ -96,6 +119,11 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
super.initViews(rootView, savedInstanceState);
infoListAdapter.useMiniItemVariants(true);
remotePlaylistManager.getPlaylist(serviceId, url)
.onBackpressureLatest()
.observeOn(AndroidSchedulers.mainThread())
.subscribe(getPlaylistBookmarkSubscriber());
}
@Override
@ -112,29 +140,26 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
context.getResources().getString(R.string.start_here_on_popup),
};
final DialogInterface.OnClickListener actions = new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
final int index = Math.max(infoListAdapter.getItemsList().indexOf(item), 0);
switch (i) {
case 0:
NavigationHelper.enqueueOnBackgroundPlayer(context, new SinglePlayQueue(item));
break;
case 1:
NavigationHelper.enqueueOnPopupPlayer(activity, new SinglePlayQueue(item));
break;
case 2:
NavigationHelper.playOnMainPlayer(context, getPlayQueue(index));
break;
case 3:
NavigationHelper.playOnBackgroundPlayer(context, getPlayQueue(index));
break;
case 4:
NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(index));
break;
default:
break;
}
final DialogInterface.OnClickListener actions = (dialogInterface, i) -> {
final int index = Math.max(infoListAdapter.getItemsList().indexOf(item), 0);
switch (i) {
case 0:
NavigationHelper.enqueueOnBackgroundPlayer(context, new SinglePlayQueue(item));
break;
case 1:
NavigationHelper.enqueueOnPopupPlayer(activity, new SinglePlayQueue(item));
break;
case 2:
NavigationHelper.playOnMainPlayer(context, getPlayQueue(index));
break;
case 3:
NavigationHelper.playOnBackgroundPlayer(context, getPlayQueue(index));
break;
case 4:
NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(index));
break;
default:
break;
}
};
@ -148,10 +173,28 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
super.onCreateOptionsMenu(menu, inflater);
inflater.inflate(R.menu.menu_playlist, menu);
playlistAppendButton = menu.findItem(R.id.menu_append_playlist);
if (currentInfo != null) {
playlistAppendButton.setVisible(!currentInfo.getRelatedStreams().isEmpty());
}
playlistBookmarkButton = menu.findItem(R.id.menu_item_bookmark);
playlistUnbookmarkButton = menu.findItem(R.id.menu_item_unbookmark);
}
@Override
public void onDestroyView() {
super.onDestroyView();
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;
}
/*//////////////////////////////////////////////////////////////////////////
@ -177,8 +220,11 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
case R.id.menu_item_share:
shareUrl(name, url);
break;
case R.id.menu_append_playlist:
appendToPlaylist(getFragmentManager(), TAG);
case R.id.menu_item_bookmark:
bookmarkPlaylist();
break;
case R.id.menu_item_unbookmark:
unbookmarkPlaylist();
break;
default:
return super.onOptionsItemSelected(item);
@ -211,12 +257,11 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
if (!TextUtils.isEmpty(result.getUploaderName())) {
headerUploaderName.setText(result.getUploaderName());
if (!TextUtils.isEmpty(result.getUploaderUrl())) {
headerUploaderLayout.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
NavigationHelper.openChannelFragment(getFragmentManager(), result.getServiceId(), result.getUploaderUrl(), result.getUploaderName());
}
});
headerUploaderLayout.setOnClickListener(v ->
NavigationHelper.openChannelFragment(getFragmentManager(),
result.getServiceId(), result.getUploaderUrl(),
result.getUploaderName())
);
}
}
@ -225,31 +270,20 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
imageLoader.displayImage(result.getUploaderAvatarUrl(), headerUploaderAvatar, DISPLAY_AVATAR_OPTIONS);
headerStreamCount.setText(getResources().getQuantityString(R.plurals.videos, (int) result.stream_count, (int) result.stream_count));
if (playlistAppendButton != null) playlistAppendButton
.setVisible(!currentInfo.getRelatedStreams().isEmpty());
if (!result.getErrors().isEmpty()) {
showSnackBarError(result.getErrors(), UserAction.REQUESTED_PLAYLIST, NewPipe.getNameOfService(result.getServiceId()), result.getUrl(), 0);
}
headerPlayAllButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
NavigationHelper.playOnMainPlayer(activity, getPlayQueue());
}
});
headerPopupButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
NavigationHelper.playOnPopupPlayer(activity, getPlayQueue());
}
});
headerBackgroundButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue());
}
});
remotePlaylistManager.onUpdate(result)
.subscribeOn(AndroidSchedulers.mainThread())
.subscribe(integer -> {/* Do nothing*/}, this::onError);
headerPlayAllButton.setOnClickListener(view ->
NavigationHelper.playOnMainPlayer(activity, getPlayQueue()));
headerPopupButton.setOnClickListener(view ->
NavigationHelper.playOnPopupPlayer(activity, getPlayQueue()));
headerBackgroundButton.setOnClickListener(view ->
NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue()));
}
private PlayQueue getPlayQueue() {
@ -293,9 +327,64 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
// Utils
//////////////////////////////////////////////////////////////////////////*/
private Subscriber<List<PlaylistRemoteEntity>> getPlaylistBookmarkSubscriber() {
return new Subscriber<List<PlaylistRemoteEntity>>() {
@Override
public void onSubscribe(Subscription s) {
if (bookmarkReactor != null) bookmarkReactor.cancel();
bookmarkReactor = s;
bookmarkReactor.request(1);
}
@Override
public void onNext(List<PlaylistRemoteEntity> playlist) {
if (playlistBookmarkButton == null || playlistUnbookmarkButton == null) return;
playlistBookmarkButton.setVisible(playlist.isEmpty());
playlistUnbookmarkButton.setVisible(!playlist.isEmpty());
playlistEntity = playlist.isEmpty() ? null : playlist.get(0);
if (bookmarkReactor != null) bookmarkReactor.request(1);
}
@Override
public void onError(Throwable t) {
PlaylistFragment.this.onError(t);
}
@Override
public void onComplete() {
}
};
}
@Override
public void setTitle(String title) {
super.setTitle(title);
headerTitleView.setText(title);
}
private void bookmarkPlaylist() {
if (remotePlaylistManager == null || currentInfo == null) return;
playlistBookmarkButton.setVisible(false);
playlistUnbookmarkButton.setVisible(false);
remotePlaylistManager.onBookmark(currentInfo)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(ignored -> {/* Do nothing */}, this::onError);
}
private void unbookmarkPlaylist() {
if (remotePlaylistManager == null || playlistEntity == null) return;
playlistBookmarkButton.setVisible(false);
playlistUnbookmarkButton.setVisible(false);
remotePlaylistManager.deletePlaylist(playlistEntity.getUid())
.observeOn(AndroidSchedulers.mainThread())
.doFinally(() -> playlistEntity = null)
.subscribe(ignored -> {/* Do nothing */}, this::onError);
}
}

View File

@ -11,12 +11,12 @@ import org.schabi.newpipe.fragments.local.holder.LocalItemHolder;
import org.schabi.newpipe.fragments.local.holder.LocalPlaylistItemHolder;
import org.schabi.newpipe.fragments.local.holder.LocalPlaylistStreamItemHolder;
import org.schabi.newpipe.fragments.local.holder.LocalStatisticStreamItemHolder;
import org.schabi.newpipe.fragments.local.holder.RemotePlaylistItemHolder;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.OnClickGesture;
import java.text.DateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/*
@ -49,8 +49,9 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
private static final int STREAM_STATISTICS_HOLDER_TYPE = 0x1000;
private static final int STREAM_PLAYLIST_HOLDER_TYPE = 0x1001;
private static final int PLAYLIST_HOLDER_TYPE = 0x2000;
private static final int LOCAL_PLAYLIST_HOLDER_TYPE = 0x2000;
private static final int REMOTE_PLAYLIST_HOLDER_TYPE = 0x2001;
private final LocalItemBuilder localItemBuilder;
private final ArrayList<LocalItem> localItems;
private final DateFormat dateFormat;
@ -187,7 +188,9 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
final LocalItem item = localItems.get(position);
switch (item.getLocalItemType()) {
case PLAYLIST_ITEM: return PLAYLIST_HOLDER_TYPE;
case PLAYLIST_LOCAL_ITEM: return LOCAL_PLAYLIST_HOLDER_TYPE;
case PLAYLIST_REMOTE_ITEM: return REMOTE_PLAYLIST_HOLDER_TYPE;
case PLAYLIST_STREAM_ITEM: return STREAM_PLAYLIST_HOLDER_TYPE;
case STATISTIC_STREAM_ITEM: return STREAM_STATISTICS_HOLDER_TYPE;
default:
@ -206,8 +209,10 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
return new HeaderFooterHolder(header);
case FOOTER_TYPE:
return new HeaderFooterHolder(footer);
case PLAYLIST_HOLDER_TYPE:
case LOCAL_PLAYLIST_HOLDER_TYPE:
return new LocalPlaylistItemHolder(localItemBuilder, parent);
case REMOTE_PLAYLIST_HOLDER_TYPE:
return new RemotePlaylistItemHolder(localItemBuilder, parent);
case STREAM_PLAYLIST_HOLDER_TYPE:
return new LocalPlaylistStreamItemHolder(localItemBuilder, parent);
case STREAM_STATISTICS_HOLDER_TYPE:

View File

@ -0,0 +1,48 @@
package org.schabi.newpipe.fragments.local;
import org.schabi.newpipe.database.AppDatabase;
import org.schabi.newpipe.database.playlist.dao.PlaylistRemoteDAO;
import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity;
import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
import java.util.List;
import io.reactivex.Flowable;
import io.reactivex.Single;
import io.reactivex.schedulers.Schedulers;
public class RemotePlaylistManager {
private final AppDatabase database;
private final PlaylistRemoteDAO playlistRemoteTable;
public RemotePlaylistManager(final AppDatabase db) {
database = db;
playlistRemoteTable = db.playlistRemoteDAO();
}
public Flowable<List<PlaylistRemoteEntity>> getPlaylists() {
return playlistRemoteTable.getAll().subscribeOn(Schedulers.io());
}
public Flowable<List<PlaylistRemoteEntity>> getPlaylist(final int serviceId, final String url) {
return playlistRemoteTable.getPlaylist(serviceId, url).subscribeOn(Schedulers.io());
}
public Single<Integer> deletePlaylist(final long playlistId) {
return Single.fromCallable(() -> playlistRemoteTable.deletePlaylist(playlistId))
.subscribeOn(Schedulers.io());
}
public Single<Long> onBookmark(final PlaylistInfo playlistInfo) {
return Single.fromCallable(() -> {
final PlaylistRemoteEntity playlist = new PlaylistRemoteEntity(playlistInfo);
return playlistRemoteTable.upsert(playlist);
}).subscribeOn(Schedulers.io());
}
public Single<Integer> onUpdate(final PlaylistInfo playlistInfo) {
return Single.fromCallable(() -> playlistRemoteTable.update(new PlaylistRemoteEntity(playlistInfo)))
.subscribeOn(Schedulers.io());
}
}

View File

@ -5,7 +5,7 @@ import android.os.Bundle;
import android.os.Parcelable;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Log;
import android.support.v4.app.FragmentManager;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@ -14,23 +14,30 @@ import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription;
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.fragments.local.BaseLocalListFragment;
import org.schabi.newpipe.fragments.local.LocalPlaylistManager;
import org.schabi.newpipe.fragments.local.RemotePlaylistManager;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.OnClickGesture;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import icepick.State;
import io.reactivex.Flowable;
import io.reactivex.Single;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.disposables.Disposable;
public final class BookmarkFragment
extends BaseLocalListFragment<List<PlaylistMetadataEntry>, Void> {
extends BaseLocalListFragment<List<PlaylistLocalItem>, Void> {
private View watchHistoryButton;
private View mostWatchedButton;
@ -41,6 +48,7 @@ public final class BookmarkFragment
private Subscription databaseSubscription;
private CompositeDisposable disposables = new CompositeDisposable();
private LocalPlaylistManager localPlaylistManager;
private RemotePlaylistManager remotePlaylistManager;
///////////////////////////////////////////////////////////////////////////
// Fragment LifeCycle - Creation
@ -49,7 +57,9 @@ public final class BookmarkFragment
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
localPlaylistManager = new LocalPlaylistManager(NewPipeDatabase.getInstance(getContext()));
final AppDatabase database = NewPipeDatabase.getInstance(getContext());
localPlaylistManager = new LocalPlaylistManager(database);
remotePlaylistManager = new RemotePlaylistManager(database);
disposables = new CompositeDisposable();
}
@ -99,17 +109,28 @@ public final class BookmarkFragment
@Override
public void selected(LocalItem selectedItem) {
// Requires the parent fragment to find holder for fragment replacement
if (selectedItem instanceof PlaylistMetadataEntry && getParentFragment() != null) {
if (getParentFragment() == null) return;
final FragmentManager fragmentManager = getParentFragment().getFragmentManager();
if (selectedItem instanceof PlaylistMetadataEntry) {
final PlaylistMetadataEntry entry = ((PlaylistMetadataEntry) selectedItem);
NavigationHelper.openLocalPlaylistFragment(
getParentFragment().getFragmentManager(), entry.uid, entry.name);
NavigationHelper.openLocalPlaylistFragment(fragmentManager, entry.uid,
entry.name);
} else if (selectedItem instanceof PlaylistRemoteEntity) {
final PlaylistRemoteEntity entry = ((PlaylistRemoteEntity) selectedItem);
NavigationHelper.openPlaylistFragment(fragmentManager, entry.getServiceId(),
entry.getUrl(), entry.getName());
}
}
@Override
public void held(LocalItem selectedItem) {
if (selectedItem instanceof PlaylistMetadataEntry) {
showDeleteDialog((PlaylistMetadataEntry) selectedItem);
showLocalDeleteDialog((PlaylistMetadataEntry) selectedItem);
} else if (selectedItem instanceof PlaylistRemoteEntity) {
showRemoteDeleteDialog((PlaylistRemoteEntity) selectedItem);
}
}
});
@ -134,9 +155,14 @@ public final class BookmarkFragment
@Override
public void startLoading(boolean forceLoad) {
super.startLoading(forceLoad);
localPlaylistManager.getPlaylists()
.observeOn(AndroidSchedulers.mainThread())
.subscribe(getSubscriptionSubscriber());
Flowable.combineLatest(
localPlaylistManager.getPlaylists(),
remotePlaylistManager.getPlaylists(),
BookmarkFragment::merge
).onBackpressureLatest()
.observeOn(AndroidSchedulers.mainThread())
.subscribe(getPlaylistsSubscriber());
}
///////////////////////////////////////////////////////////////////////////
@ -165,6 +191,7 @@ public final class BookmarkFragment
disposables = null;
localPlaylistManager = null;
remotePlaylistManager = null;
itemsListState = null;
}
@ -172,8 +199,8 @@ public final class BookmarkFragment
// Subscriptions Loader
///////////////////////////////////////////////////////////////////////////
private Subscriber<List<PlaylistMetadataEntry>> getSubscriptionSubscriber() {
return new Subscriber<List<PlaylistMetadataEntry>>() {
private Subscriber<List<PlaylistLocalItem>> getPlaylistsSubscriber() {
return new Subscriber<List<PlaylistLocalItem>>() {
@Override
public void onSubscribe(Subscription s) {
showLoading();
@ -183,7 +210,7 @@ public final class BookmarkFragment
}
@Override
public void onNext(List<PlaylistMetadataEntry> subscriptions) {
public void onNext(List<PlaylistLocalItem> subscriptions) {
handleResult(subscriptions);
if (databaseSubscription != null) databaseSubscription.request(1);
}
@ -200,7 +227,7 @@ public final class BookmarkFragment
}
@Override
public void handleResult(@NonNull List<PlaylistMetadataEntry> result) {
public void handleResult(@NonNull List<PlaylistLocalItem> result) {
super.handleResult(result);
itemListAdapter.clearStreamItemList();
@ -240,25 +267,41 @@ public final class BookmarkFragment
// Utils
///////////////////////////////////////////////////////////////////////////
private void showDeleteDialog(final PlaylistMetadataEntry item) {
private void showLocalDeleteDialog(final PlaylistMetadataEntry item) {
showDeleteDialog(item.name, localPlaylistManager.deletePlaylist(item.uid));
}
private void showRemoteDeleteDialog(final PlaylistRemoteEntity item) {
showDeleteDialog(item.getName(), remotePlaylistManager.deletePlaylist(item.getUid()));
}
private void showDeleteDialog(final String name, final Single<Integer> deleteReactor) {
if (activity == null || disposables == null) return;
new AlertDialog.Builder(activity)
.setTitle(item.name)
.setTitle(name)
.setMessage(R.string.delete_playlist_prompt)
.setCancelable(true)
.setPositiveButton(R.string.delete, (dialog, i) ->
disposables.add(deletePlaylist(item.uid))
disposables.add(deleteReactor
.observeOn(AndroidSchedulers.mainThread())
.subscribe(ignored -> {/*Do nothing on success*/}, this::onError))
)
.setNegativeButton(R.string.cancel, null)
.show();
}
private Disposable deletePlaylist(final long playlistId) {
return localPlaylistManager.deletePlaylist(playlistId)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(ignored -> {/*Do nothing on success*/},
throwable -> Log.e(TAG, "Playlist deletion failed, id=["
+ playlistId + "]")
);
private static List<PlaylistLocalItem> merge(final List<PlaylistMetadataEntry> localPlaylists,
final List<PlaylistRemoteEntity> remotePlaylists) {
List<PlaylistLocalItem> items = new ArrayList<>(
localPlaylists.size() + remotePlaylists.size());
items.addAll(localPlaylists);
items.addAll(remotePlaylists);
Collections.sort(items, (left, right) ->
left.getOrderingName().compareToIgnoreCase(right.getOrderingName()));
return items;
}
}

View File

@ -14,24 +14,10 @@ import org.schabi.newpipe.fragments.local.LocalItemBuilder;
import java.text.DateFormat;
public class LocalPlaylistItemHolder extends LocalItemHolder {
public final ImageView itemThumbnailView;
public final TextView itemStreamCountView;
public final TextView itemTitleView;
public final TextView itemUploaderView;
public LocalPlaylistItemHolder(LocalItemBuilder infoItemBuilder,
int layoutId, ViewGroup parent) {
super(infoItemBuilder, layoutId, parent);
itemThumbnailView = itemView.findViewById(R.id.itemThumbnailView);
itemTitleView = itemView.findViewById(R.id.itemTitleView);
itemStreamCountView = itemView.findViewById(R.id.itemStreamCountView);
itemUploaderView = itemView.findViewById(R.id.itemUploaderView);
}
public class LocalPlaylistItemHolder extends PlaylistItemHolder {
public LocalPlaylistItemHolder(LocalItemBuilder infoItemBuilder, ViewGroup parent) {
this(infoItemBuilder, R.layout.list_playlist_mini_item, parent);
super(infoItemBuilder, parent);
}
@Override
@ -45,29 +31,6 @@ public class LocalPlaylistItemHolder extends LocalItemHolder {
itemBuilder.displayImage(item.thumbnailUrl, itemThumbnailView, DISPLAY_THUMBNAIL_OPTIONS);
itemView.setOnClickListener(view -> {
if (itemBuilder.getOnItemSelectedListener() != null) {
itemBuilder.getOnItemSelectedListener().selected(item);
}
});
itemView.setLongClickable(true);
itemView.setOnLongClickListener(view -> {
if (itemBuilder.getOnItemSelectedListener() != null) {
itemBuilder.getOnItemSelectedListener().held(item);
}
return true;
});
super.updateFromItem(localItem, dateFormat);
}
/**
* Display options for playlist thumbnails
*/
public static final DisplayImageOptions DISPLAY_THUMBNAIL_OPTIONS =
new DisplayImageOptions.Builder()
.cloneFrom(BASE_DISPLAY_IMAGE_OPTIONS)
.showImageOnLoading(R.drawable.dummy_thumbnail_playlist)
.showImageForEmptyUri(R.drawable.dummy_thumbnail_playlist)
.showImageOnFail(R.drawable.dummy_thumbnail_playlist)
.build();
}

View File

@ -0,0 +1,62 @@
package org.schabi.newpipe.fragments.local.holder;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import com.nostra13.universalimageloader.core.DisplayImageOptions;
import org.schabi.newpipe.R;
import org.schabi.newpipe.database.LocalItem;
import org.schabi.newpipe.fragments.local.LocalItemBuilder;
import java.text.DateFormat;
public abstract class PlaylistItemHolder extends LocalItemHolder {
public final ImageView itemThumbnailView;
public final TextView itemStreamCountView;
public final TextView itemTitleView;
public final TextView itemUploaderView;
public PlaylistItemHolder(LocalItemBuilder infoItemBuilder,
int layoutId, ViewGroup parent) {
super(infoItemBuilder, layoutId, parent);
itemThumbnailView = itemView.findViewById(R.id.itemThumbnailView);
itemTitleView = itemView.findViewById(R.id.itemTitleView);
itemStreamCountView = itemView.findViewById(R.id.itemStreamCountView);
itemUploaderView = itemView.findViewById(R.id.itemUploaderView);
}
public PlaylistItemHolder(LocalItemBuilder infoItemBuilder, ViewGroup parent) {
this(infoItemBuilder, R.layout.list_playlist_mini_item, parent);
}
@Override
public void updateFromItem(final LocalItem localItem, final DateFormat dateFormat) {
itemView.setOnClickListener(view -> {
if (itemBuilder.getOnItemSelectedListener() != null) {
itemBuilder.getOnItemSelectedListener().selected(localItem);
}
});
itemView.setLongClickable(true);
itemView.setOnLongClickListener(view -> {
if (itemBuilder.getOnItemSelectedListener() != null) {
itemBuilder.getOnItemSelectedListener().held(localItem);
}
return true;
});
}
/**
* Display options for playlist thumbnails
*/
public static final DisplayImageOptions DISPLAY_THUMBNAIL_OPTIONS =
new DisplayImageOptions.Builder()
.cloneFrom(BASE_DISPLAY_IMAGE_OPTIONS)
.showImageOnLoading(R.drawable.dummy_thumbnail_playlist)
.showImageForEmptyUri(R.drawable.dummy_thumbnail_playlist)
.showImageOnFail(R.drawable.dummy_thumbnail_playlist)
.build();
}

View File

@ -0,0 +1,33 @@
package org.schabi.newpipe.fragments.local.holder;
import android.view.ViewGroup;
import org.schabi.newpipe.database.LocalItem;
import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.fragments.local.LocalItemBuilder;
import org.schabi.newpipe.util.Localization;
import java.text.DateFormat;
public class RemotePlaylistItemHolder extends PlaylistItemHolder {
public RemotePlaylistItemHolder(LocalItemBuilder infoItemBuilder, ViewGroup parent) {
super(infoItemBuilder, parent);
}
@Override
public void updateFromItem(final LocalItem localItem, final DateFormat dateFormat) {
if (!(localItem instanceof PlaylistRemoteEntity)) return;
final PlaylistRemoteEntity item = (PlaylistRemoteEntity) localItem;
itemTitleView.setText(item.getName());
itemStreamCountView.setText(String.valueOf(item.getStreamCount()));
itemUploaderView.setText(Localization.concatenateStrings(item.getUploader(),
NewPipe.getNameOfService(item.getServiceId())));
itemBuilder.displayImage(item.getThumbnailUrl(), itemThumbnailView,
DISPLAY_THUMBNAIL_OPTIONS);
super.updateFromItem(localItem, dateFormat);
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 163 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 159 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 163 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 163 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 236 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 236 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 283 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 289 B

View File

@ -314,7 +314,7 @@
android:clickable="true"
android:focusable="true"
android:contentDescription="@string/append_playlist"
android:drawableTop="?attr/playlist_add"
android:drawableTop="?attr/ic_playlist_add"
android:gravity="center"
android:paddingBottom="6dp"
android:paddingTop="6dp"

View File

@ -22,12 +22,4 @@
android:icon="?attr/share"
android:title="@string/share"
app:showAsAction="ifRoom"/>
<item
android:id="@+id/menu_append_playlist"
android:icon="?attr/playlist_add"
android:title="@string/append_playlist"
android:visible="false"
app:showAsAction="ifRoom"
tools:visible="true"/>
</menu>

View File

@ -5,7 +5,7 @@
<item
android:id="@+id/action_append_playlist"
android:icon="?attr/playlist_add"
android:icon="?attr/ic_playlist_add"
android:title="@string/append_playlist"
android:visible="true"
app:showAsAction="ifRoom"/>

View File

@ -15,10 +15,18 @@
app:showAsAction="ifRoom"/>
<item
android:id="@+id/menu_append_playlist"
android:icon="?attr/playlist_add"
android:title="@string/append_playlist"
android:id="@+id/menu_item_bookmark"
android:icon="?attr/ic_playlist_add"
android:title="@string/bookmark_playlist"
android:visible="false"
app:showAsAction="ifRoom"
app:showAsAction="always"
tools:visible="true"/>
<item
android:id="@+id/menu_item_unbookmark"
android:icon="?attr/ic_playlist_check"
android:title="@string/unbookmark_playlist"
android:visible="false"
app:showAsAction="always"
tools:visible="true"/>
</menu>

View File

@ -27,7 +27,8 @@
<attr name="ic_hot" format="reference"/>
<attr name="ic_channel" format="reference"/>
<attr name="ic_bookmark" format="reference"/>
<attr name="playlist_add" format="reference"/>
<attr name="ic_playlist_add" format="reference"/>
<attr name="ic_playlist_check" format="reference"/>
<!-- Can't refer to colors directly into drawable's xml-->
<attr name="toolbar_shadow_drawable" format="reference"/>

View File

@ -387,6 +387,9 @@
<string name="append_playlist">Add To Playlist</string>
<string name="set_as_playlist_thumbnail">Set as Playlist Thumbnail</string>
<string name="bookmark_playlist">Bookmark Playlist</string>
<string name="unbookmark_playlist">Remove Bookmark</string>
<string name="delete_playlist_prompt">Do you want to delete this playlist?</string>
<string name="playlist_creation_success">Playlist successfully created</string>
<string name="playlist_add_stream_success">Added to playlist</string>

View File

@ -42,7 +42,8 @@
<item name="ic_hot">@drawable/ic_whatshot_black_24dp</item>
<item name="ic_channel">@drawable/ic_channel_black_24dp</item>
<item name="ic_bookmark">@drawable/ic_bookmark_black_24dp</item>
<item name="playlist_add">@drawable/ic_playlist_add_black_24dp</item>
<item name="ic_playlist_add">@drawable/ic_playlist_add_black_24dp</item>
<item name="ic_playlist_check">@drawable/ic_playlist_add_check_black_24dp</item>
<item name="separator_color">@color/light_separator_color</item>
<item name="contrast_background_color">@color/light_contrast_background_color</item>
@ -91,7 +92,8 @@
<item name="ic_hot">@drawable/ic_whatshot_white_24dp</item>
<item name="ic_channel">@drawable/ic_channel_white_24dp</item>
<item name="ic_bookmark">@drawable/ic_bookmark_white_24dp</item>
<item name="playlist_add">@drawable/ic_playlist_add_white_24dp</item>
<item name="ic_playlist_add">@drawable/ic_playlist_add_white_24dp</item>
<item name="ic_playlist_check">@drawable/ic_playlist_add_check_white_24dp</item>
<item name="separator_color">@color/dark_separator_color</item>
<item name="contrast_background_color">@color/dark_contrast_background_color</item>