-Added support for changing local playlist name and thumbnail url.

-Added query to remove stream table orphans.
-Added query for retrieving flattened watch history records.
-Added holder for local playlist stream info items.
-Refactored info item on select listener as on touch gesture.
This commit is contained in:
John Zhen Mo 2018-01-25 22:24:59 -08:00
parent 81f481833c
commit f0829f9ef3
19 changed files with 353 additions and 68 deletions

View File

@ -0,0 +1,47 @@
package org.schabi.newpipe.database.stream;
import android.arch.persistence.room.ColumnInfo;
import org.schabi.newpipe.database.stream.model.StreamEntity;
import org.schabi.newpipe.database.stream.model.StreamHistoryEntity;
import org.schabi.newpipe.extractor.stream.StreamType;
import java.util.Date;
public class StreamHistoryEntry {
@ColumnInfo(name = StreamEntity.STREAM_ID)
final public long uid;
@ColumnInfo(name = StreamEntity.STREAM_SERVICE_ID)
final public int serviceId;
@ColumnInfo(name = StreamEntity.STREAM_URL)
final public String url;
@ColumnInfo(name = StreamEntity.STREAM_TITLE)
final public String title;
@ColumnInfo(name = StreamEntity.STREAM_TYPE)
final public StreamType streamType;
@ColumnInfo(name = StreamEntity.STREAM_DURATION)
final public long duration;
@ColumnInfo(name = StreamEntity.STREAM_UPLOADER)
final public String uploader;
@ColumnInfo(name = StreamEntity.STREAM_THUMBNAIL_URL)
final public String thumbnailUrl;
@ColumnInfo(name = StreamHistoryEntity.JOIN_STREAM_ID)
final public long streamId;
@ColumnInfo(name = StreamHistoryEntity.STREAM_ACCESS_DATE)
final public Date accessDate;
public StreamHistoryEntry(long uid, int serviceId, String url, String title,
StreamType streamType, long duration, String uploader,
String thumbnailUrl, long streamId, Date accessDate) {
this.uid = uid;
this.serviceId = serviceId;
this.url = url;
this.title = title;
this.streamType = streamType;
this.duration = duration;
this.uploader = uploader;
this.thumbnailUrl = thumbnailUrl;
this.streamId = streamId;
this.accessDate = accessDate;
}
}

View File

@ -7,17 +7,23 @@ 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.PlaylistStreamEntity;
import org.schabi.newpipe.database.stream.model.StreamEntity;
import org.schabi.newpipe.database.stream.model.StreamHistoryEntity;
import org.schabi.newpipe.database.stream.model.StreamStateEntity;
import java.util.ArrayList;
import java.util.List;
import io.reactivex.Flowable;
import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.PLAYLIST_STREAM_JOIN_TABLE;
import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_ID;
import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_SERVICE_ID;
import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_TABLE;
import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_URL;
import static org.schabi.newpipe.database.stream.model.StreamHistoryEntity.STREAM_HISTORY_TABLE;
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_STATE_TABLE;
@Dao
public abstract class StreamDAO implements BasicDAO<StreamEntity> {
@ -77,4 +83,22 @@ public abstract class StreamDAO implements BasicDAO<StreamEntity> {
update(streams);
return streamIds;
}
@Query("DELETE FROM " + STREAM_TABLE + " WHERE " + STREAM_ID +
" NOT IN " +
"(SELECT DISTINCT " + STREAM_ID + " FROM " + STREAM_TABLE +
" LEFT JOIN " + STREAM_HISTORY_TABLE +
" ON " + STREAM_ID + " = " +
StreamHistoryEntity.STREAM_HISTORY_TABLE + "." + StreamHistoryEntity.JOIN_STREAM_ID +
" LEFT JOIN " + STREAM_STATE_TABLE +
" ON " + STREAM_ID + " = " +
StreamStateEntity.STREAM_STATE_TABLE + "." + StreamStateEntity.JOIN_STREAM_ID +
" LEFT JOIN " + PLAYLIST_STREAM_JOIN_TABLE +
" ON " + STREAM_ID + " = " +
PlaylistStreamEntity.PLAYLIST_STREAM_JOIN_TABLE + "." + PlaylistStreamEntity.JOIN_STREAM_ID +
")")
public abstract int deleteOrphans();
}

View File

@ -6,6 +6,7 @@ import android.arch.persistence.room.Query;
import android.arch.persistence.room.Transaction;
import org.schabi.newpipe.database.BasicDAO;
import org.schabi.newpipe.database.stream.StreamHistoryEntry;
import org.schabi.newpipe.database.stream.StreamStatisticsEntry;
import org.schabi.newpipe.database.stream.model.StreamHistoryEntity;
@ -36,6 +37,12 @@ public abstract class StreamHistoryDAO implements BasicDAO<StreamHistoryEntity>
throw new UnsupportedOperationException();
}
@Query("SELECT * FROM " + STREAM_TABLE +
" INNER JOIN " + STREAM_HISTORY_TABLE +
" ON " + STREAM_ID + " = " + JOIN_STREAM_ID +
" ORDER BY " + STREAM_ACCESS_DATE + " DESC")
public abstract Flowable<List<StreamHistoryEntry>> getHistory();
@Query("DELETE FROM " + STREAM_HISTORY_TABLE + " WHERE " + JOIN_STREAM_ID + " = :streamId")
public abstract int deleteStreamHistory(final long streamId);

View File

@ -62,6 +62,7 @@ import org.schabi.newpipe.fragments.local.PlaylistAppendDialog;
import org.schabi.newpipe.history.HistoryListener;
import org.schabi.newpipe.info_list.InfoItemBuilder;
import org.schabi.newpipe.info_list.InfoItemDialog;
import org.schabi.newpipe.info_list.OnInfoItemGesture;
import org.schabi.newpipe.player.MainVideoPlayer;
import org.schabi.newpipe.player.PopupVideoPlayer;
import org.schabi.newpipe.player.helper.PlayerHelper;
@ -471,7 +472,7 @@ public class VideoDetailFragment extends BaseStateFragment<StreamInfo> implement
@Override
protected void initListeners() {
super.initListeners();
infoItemBuilder.setOnStreamSelectedListener(new InfoItemBuilder.OnInfoItemSelectedListener<StreamInfoItem>() {
infoItemBuilder.setOnStreamSelectedListener(new OnInfoItemGesture<StreamInfoItem>() {
@Override
public void selected(StreamInfoItem selectedItem) {
selectAndLoadVideo(selectedItem.getServiceId(), selectedItem.getUrl(), selectedItem.getName());

View File

@ -3,19 +3,15 @@ package org.schabi.newpipe.fragments.list;
import android.app.Activity;
import android.content.Context;
import android.content.DialogInterface;
import android.os.Build;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.v7.app.ActionBar;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.view.Gravity;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.View;
import android.widget.TextView;
import android.widget.Toast;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.InfoItem;
@ -24,12 +20,11 @@ import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.fragments.BaseStateFragment;
import org.schabi.newpipe.fragments.OnScrollBelowItemsListener;
import org.schabi.newpipe.info_list.InfoItemBuilder;
import org.schabi.newpipe.info_list.InfoItemDialog;
import org.schabi.newpipe.info_list.InfoListAdapter;
import org.schabi.newpipe.info_list.OnInfoItemGesture;
import org.schabi.newpipe.playlist.SinglePlayQueue;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.PermissionHelper;
import org.schabi.newpipe.util.StateSaver;
import java.util.List;
@ -140,7 +135,7 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> implem
@Override
protected void initListeners() {
super.initListeners();
infoListAdapter.setOnStreamSelectedListener(new InfoItemBuilder.OnInfoItemSelectedListener<StreamInfoItem>() {
infoListAdapter.setOnStreamSelectedListener(new OnInfoItemGesture<StreamInfoItem>() {
@Override
public void selected(StreamInfoItem selectedItem) {
onItemSelected(selectedItem);
@ -155,7 +150,7 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> implem
}
});
infoListAdapter.setOnChannelSelectedListener(new InfoItemBuilder.OnInfoItemSelectedListener<ChannelInfoItem>() {
infoListAdapter.setOnChannelSelectedListener(new OnInfoItemGesture<ChannelInfoItem>() {
@Override
public void selected(ChannelInfoItem selectedItem) {
onItemSelected(selectedItem);
@ -163,12 +158,9 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> implem
useAsFrontPage?getParentFragment().getFragmentManager():getFragmentManager(),
selectedItem.getServiceId(), selectedItem.getUrl(), selectedItem.getName());
}
@Override
public void held(ChannelInfoItem selectedItem) {}
});
infoListAdapter.setOnPlaylistSelectedListener(new InfoItemBuilder.OnInfoItemSelectedListener<PlaylistInfoItem>() {
infoListAdapter.setOnPlaylistSelectedListener(new OnInfoItemGesture<PlaylistInfoItem>() {
@Override
public void selected(PlaylistInfoItem selectedItem) {
onItemSelected(selectedItem);
@ -176,9 +168,6 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> implem
useAsFrontPage?getParentFragment().getFragmentManager():getFragmentManager(),
selectedItem.getServiceId(), selectedItem.getUrl(), selectedItem.getName());
}
@Override
public void held(PlaylistInfoItem selectedItem) {}
});
itemsList.clearOnScrollListeners();

View File

@ -21,9 +21,9 @@ import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem;
import org.schabi.newpipe.fragments.BaseStateFragment;
import org.schabi.newpipe.info_list.InfoItemBuilder;
import org.schabi.newpipe.info_list.InfoItemDialog;
import org.schabi.newpipe.info_list.InfoListAdapter;
import org.schabi.newpipe.info_list.OnInfoItemGesture;
import org.schabi.newpipe.info_list.stored.LocalPlaylistInfoItem;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.NavigationHelper;
@ -33,10 +33,8 @@ import java.util.Collections;
import java.util.List;
import icepick.State;
import io.reactivex.Observer;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.disposables.Disposable;
import static org.schabi.newpipe.util.AnimationUtils.animateView;
@ -143,7 +141,7 @@ public class BookmarkFragment extends BaseStateFragment<List<PlaylistMetadataEnt
protected void initListeners() {
super.initListeners();
infoListAdapter.setOnPlaylistSelectedListener(new InfoItemBuilder.OnInfoItemSelectedListener<PlaylistInfoItem>() {
infoListAdapter.setOnPlaylistSelectedListener(new OnInfoItemGesture<PlaylistInfoItem>() {
@Override
public void selected(PlaylistInfoItem selectedItem) {
// Requires the parent fragment to find holder for fragment replacement

View File

@ -21,8 +21,8 @@ import org.schabi.newpipe.database.stream.model.StreamEntity;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.fragments.list.BaseListFragment;
import org.schabi.newpipe.info_list.InfoItemBuilder;
import org.schabi.newpipe.info_list.InfoItemDialog;
import org.schabi.newpipe.info_list.OnInfoItemGesture;
import org.schabi.newpipe.playlist.PlayQueue;
import org.schabi.newpipe.playlist.SinglePlayQueue;
import org.schabi.newpipe.report.UserAction;
@ -141,7 +141,7 @@ public class LocalPlaylistFragment extends BaseListFragment<List<StreamEntity>,
protected void initListeners() {
super.initListeners();
infoListAdapter.setOnStreamSelectedListener(new InfoItemBuilder.OnInfoItemSelectedListener<StreamInfoItem>() {
infoListAdapter.setOnStreamSelectedListener(new OnInfoItemGesture<StreamInfoItem>() {
@Override
public void selected(StreamInfoItem selectedItem) {
// Requires the parent fragment to find holder for fragment replacement
@ -219,7 +219,7 @@ public class LocalPlaylistFragment extends BaseListFragment<List<StreamEntity>,
super.startLoading(forceLoad);
resetFragment();
playlistManager.getPlaylist(playlistId)
playlistManager.getPlaylistStreams(playlistId)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(getPlaylistObserver());
}
@ -317,7 +317,7 @@ public class LocalPlaylistFragment extends BaseListFragment<List<StreamEntity>,
if (super.onError(exception)) return true;
onUnrecoverableError(exception, UserAction.SOMETHING_ELSE,
"none", "Subscriptions", R.string.general_error);
"none", "Local Playlist", R.string.general_error);
return true;
}

View File

@ -1,5 +1,7 @@
package org.schabi.newpipe.fragments.local;
import android.support.annotation.Nullable;
import org.schabi.newpipe.database.AppDatabase;
import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry;
import org.schabi.newpipe.database.playlist.dao.PlaylistDAO;
@ -82,7 +84,7 @@ public class LocalPlaylistManager {
return playlistStreamTable.getPlaylistMetadata().subscribeOn(Schedulers.io());
}
public Flowable<List<StreamEntity>> getPlaylist(final long playlistId) {
public Flowable<List<StreamEntity>> getPlaylistStreams(final long playlistId) {
return playlistStreamTable.getOrderedStreamsOf(playlistId).subscribeOn(Schedulers.io());
}
@ -90,4 +92,28 @@ public class LocalPlaylistManager {
return Single.fromCallable(() -> playlistTable.deletePlaylist(playlistId))
.subscribeOn(Schedulers.io());
}
public Maybe<Integer> renamePlaylist(final long playlistId, final String name) {
return modifyPlaylist(playlistId, name, null);
}
public Maybe<Integer> changePlaylistThumbnail(final long playlistId,
final String thumbnailUrl) {
return modifyPlaylist(playlistId, null, thumbnailUrl);
}
private Maybe<Integer> modifyPlaylist(final long playlistId,
@Nullable final String name,
@Nullable final String thumbnailUrl) {
return playlistTable.getPlaylist(playlistId)
.firstElement()
.filter(playlistEntities -> !playlistEntities.isEmpty())
.map(playlistEntities -> {
PlaylistEntity playlist = playlistEntities.get(0);
if (name != null) playlist.setName(name);
if (thumbnailUrl != null) playlist.setThumbnailUrl(thumbnailUrl);
return playlistTable.update(playlist);
}).subscribeOn(Schedulers.io());
}
}

View File

@ -19,8 +19,8 @@ import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.info_list.InfoItemBuilder;
import org.schabi.newpipe.info_list.InfoListAdapter;
import org.schabi.newpipe.info_list.OnInfoItemGesture;
import org.schabi.newpipe.info_list.stored.LocalPlaylistInfoItem;
import org.schabi.newpipe.playlist.PlayQueueItem;
@ -97,7 +97,7 @@ public final class PlaylistAppendDialog extends PlaylistDialog {
newPlaylistButton.setOnClickListener(ignored -> openCreatePlaylistDialog());
playlistAdapter.setOnPlaylistSelectedListener(new InfoItemBuilder.OnInfoItemSelectedListener<PlaylistInfoItem>() {
playlistAdapter.setOnPlaylistSelectedListener(new OnInfoItemGesture<PlaylistInfoItem>() {
@Override
public void selected(PlaylistInfoItem selectedItem) {
if (!(selectedItem instanceof LocalPlaylistInfoItem) || getStreams() == null)
@ -113,9 +113,6 @@ public final class PlaylistAppendDialog extends PlaylistDialog {
getDialog().dismiss();
}
@Override
public void held(PlaylistInfoItem selectedItem) {}
});
playlistManager.getPlaylists()

View File

@ -35,8 +35,7 @@ public final class PlaylistCreationDialog extends PlaylistDialog {
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
if (getStreams() == null) return super.onCreateDialog(savedInstanceState);
View dialogView = View.inflate(getContext(),
R.layout.dialog_create_playlist, null);
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())

View File

@ -19,8 +19,8 @@ import org.schabi.newpipe.database.stream.StreamStatisticsEntry;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.fragments.list.BaseListFragment;
import org.schabi.newpipe.info_list.InfoItemBuilder;
import org.schabi.newpipe.info_list.InfoItemDialog;
import org.schabi.newpipe.info_list.OnInfoItemGesture;
import org.schabi.newpipe.info_list.stored.StreamStatisticsInfoItem;
import org.schabi.newpipe.playlist.PlayQueue;
import org.schabi.newpipe.playlist.SinglePlayQueue;
@ -127,7 +127,7 @@ public abstract class StatisticsPlaylistFragment
protected void initListeners() {
super.initListeners();
infoListAdapter.setOnStreamSelectedListener(new InfoItemBuilder.OnInfoItemSelectedListener<StreamInfoItem>() {
infoListAdapter.setOnStreamSelectedListener(new OnInfoItemGesture<StreamInfoItem>() {
@Override
public void selected(StreamInfoItem selectedItem) {
NavigationHelper.openVideoDetailFragment(getFragmentManager(),

View File

@ -16,8 +16,8 @@ import org.schabi.newpipe.database.subscription.SubscriptionEntity;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
import org.schabi.newpipe.fragments.BaseStateFragment;
import org.schabi.newpipe.info_list.InfoItemBuilder;
import org.schabi.newpipe.info_list.InfoListAdapter;
import org.schabi.newpipe.info_list.OnInfoItemGesture;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.NavigationHelper;
@ -125,24 +125,17 @@ public class SubscriptionFragment extends BaseStateFragment<List<SubscriptionEnt
protected void initListeners() {
super.initListeners();
infoListAdapter.setOnChannelSelectedListener(new InfoItemBuilder.OnInfoItemSelectedListener<ChannelInfoItem>() {
infoListAdapter.setOnChannelSelectedListener(new OnInfoItemGesture<ChannelInfoItem>() {
@Override
public void selected(ChannelInfoItem selectedItem) {
// Requires the parent fragment to find holder for fragment replacement
NavigationHelper.openChannelFragment(getParentFragment().getFragmentManager(), selectedItem.getServiceId(), selectedItem.url, selectedItem.getName());
}
@Override
public void held(ChannelInfoItem selectedItem) {}
});
headerRootLayout.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
NavigationHelper.openWhatsNewFragment(getParentFragment().getFragmentManager());
NavigationHelper.openChannelFragment(getParentFragment().getFragmentManager(),
selectedItem.getServiceId(), selectedItem.url, selectedItem.getName());
}
});
headerRootLayout.setOnClickListener(view ->
NavigationHelper.openWhatsNewFragment(getParentFragment().getFragmentManager()));
}
private void resetFragment() {

View File

@ -43,17 +43,12 @@ import org.schabi.newpipe.info_list.holder.StreamMiniInfoItemHolder;
public class InfoItemBuilder {
private static final String TAG = InfoItemBuilder.class.toString();
public interface OnInfoItemSelectedListener<T extends InfoItem> {
void selected(T selectedItem);
void held(T selectedItem);
}
private final Context context;
private ImageLoader imageLoader = ImageLoader.getInstance();
private OnInfoItemSelectedListener<StreamInfoItem> onStreamSelectedListener;
private OnInfoItemSelectedListener<ChannelInfoItem> onChannelSelectedListener;
private OnInfoItemSelectedListener<PlaylistInfoItem> onPlaylistSelectedListener;
private OnInfoItemGesture<StreamInfoItem> onStreamSelectedListener;
private OnInfoItemGesture<ChannelInfoItem> onChannelSelectedListener;
private OnInfoItemGesture<PlaylistInfoItem> onPlaylistSelectedListener;
public InfoItemBuilder(Context context) {
this.context = context;
@ -91,27 +86,27 @@ public class InfoItemBuilder {
return imageLoader;
}
public OnInfoItemSelectedListener<StreamInfoItem> getOnStreamSelectedListener() {
public OnInfoItemGesture<StreamInfoItem> getOnStreamSelectedListener() {
return onStreamSelectedListener;
}
public void setOnStreamSelectedListener(OnInfoItemSelectedListener<StreamInfoItem> listener) {
public void setOnStreamSelectedListener(OnInfoItemGesture<StreamInfoItem> listener) {
this.onStreamSelectedListener = listener;
}
public OnInfoItemSelectedListener<ChannelInfoItem> getOnChannelSelectedListener() {
public OnInfoItemGesture<ChannelInfoItem> getOnChannelSelectedListener() {
return onChannelSelectedListener;
}
public void setOnChannelSelectedListener(OnInfoItemSelectedListener<ChannelInfoItem> listener) {
public void setOnChannelSelectedListener(OnInfoItemGesture<ChannelInfoItem> listener) {
this.onChannelSelectedListener = listener;
}
public OnInfoItemSelectedListener<PlaylistInfoItem> getOnPlaylistSelectedListener() {
public OnInfoItemGesture<PlaylistInfoItem> getOnPlaylistSelectedListener() {
return onPlaylistSelectedListener;
}
public void setOnPlaylistSelectedListener(OnInfoItemSelectedListener<PlaylistInfoItem> listener) {
public void setOnPlaylistSelectedListener(OnInfoItemGesture<PlaylistInfoItem> listener) {
this.onPlaylistSelectedListener = listener;
}

View File

@ -10,7 +10,6 @@ import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.info_list.InfoItemBuilder.OnInfoItemSelectedListener;
import org.schabi.newpipe.info_list.holder.ChannelInfoItemHolder;
import org.schabi.newpipe.info_list.holder.ChannelMiniInfoItemHolder;
import org.schabi.newpipe.info_list.holder.InfoItemHolder;
@ -56,6 +55,10 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
private static final int MINI_PLAYLIST_HOLDER_TYPE = 0x300;
private static final int PLAYLIST_HOLDER_TYPE = 0x301;
private static final int STATISTICS_HOLDER_TYPE = 0x1000;
private static final int LOCAL_PLAYLIST_STREAM_HOLDER_TYPE = 0x1001;
private static final int LOCAL_PLAYLIST_HOLDER_TYPE = 0x3000;
private final InfoItemBuilder infoItemBuilder;
private final ArrayList<InfoItem> infoItemList;
private boolean useMiniVariant = false;
@ -77,15 +80,15 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
infoItemList = new ArrayList<>();
}
public void setOnStreamSelectedListener(OnInfoItemSelectedListener<StreamInfoItem> listener) {
public void setOnStreamSelectedListener(OnInfoItemGesture<StreamInfoItem> listener) {
infoItemBuilder.setOnStreamSelectedListener(listener);
}
public void setOnChannelSelectedListener(OnInfoItemSelectedListener<ChannelInfoItem> listener) {
public void setOnChannelSelectedListener(OnInfoItemGesture<ChannelInfoItem> listener) {
infoItemBuilder.setOnChannelSelectedListener(listener);
}
public void setOnPlaylistSelectedListener(OnInfoItemSelectedListener<PlaylistInfoItem> listener) {
public void setOnPlaylistSelectedListener(OnInfoItemGesture<PlaylistInfoItem> listener) {
infoItemBuilder.setOnPlaylistSelectedListener(listener);
}
@ -202,7 +205,7 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
if (footer != null && position == infoItemList.size() && showFooter) {
return FOOTER_TYPE;
}
InfoItem item = infoItemList.get(position);
final InfoItem item = infoItemList.get(position);
switch (item.info_type) {
case STREAM:
return useMiniVariant ? MINI_STREAM_HOLDER_TYPE : STREAM_HOLDER_TYPE;

View File

@ -0,0 +1,18 @@
package org.schabi.newpipe.info_list;
import android.support.v7.widget.RecyclerView;
import org.schabi.newpipe.extractor.InfoItem;
public abstract class OnInfoItemGesture<T extends InfoItem> {
public abstract void selected(T selectedItem);
public void held(T selectedItem) {
// Optional gesture
}
public void drag(T selectedItem, RecyclerView.ViewHolder viewHolder) {
// Optional gesture
}
}

View File

@ -0,0 +1,102 @@
package org.schabi.newpipe.info_list.holder;
import android.support.v4.content.ContextCompat;
import android.support.v7.widget.RecyclerView;
import android.view.MotionEvent;
import android.view.View;
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.extractor.InfoItem;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.info_list.InfoItemBuilder;
import org.schabi.newpipe.util.Localization;
public class StreamPlaylistInfoItemHolder extends InfoItemHolder {
public final ImageView itemThumbnailView;
public final TextView itemVideoTitleView;
public final TextView itemUploaderView;
public final TextView itemDurationView;
public final View itemHandleView;
StreamPlaylistInfoItemHolder(InfoItemBuilder infoItemBuilder, int layoutId, ViewGroup parent) {
super(infoItemBuilder, layoutId, parent);
itemThumbnailView = itemView.findViewById(R.id.itemThumbnailView);
itemVideoTitleView = itemView.findViewById(R.id.itemVideoTitleView);
itemUploaderView = itemView.findViewById(R.id.itemUploaderView);
itemDurationView = itemView.findViewById(R.id.itemDurationView);
itemHandleView = itemView.findViewById(R.id.itemHandle);
}
public StreamPlaylistInfoItemHolder(InfoItemBuilder infoItemBuilder, ViewGroup parent) {
this(infoItemBuilder, R.layout.list_playlist_mini_item, parent);
}
@Override
public void updateFromItem(final InfoItem infoItem) {
if (!(infoItem instanceof StreamInfoItem)) return;
final StreamInfoItem item = (StreamInfoItem) infoItem;
itemVideoTitleView.setText(item.getName());
itemUploaderView.setText(item.uploader_name);
if (item.duration > 0) {
itemDurationView.setText(Localization.getDurationString(item.duration));
itemDurationView.setBackgroundColor(ContextCompat.getColor(itemBuilder.getContext(),
R.color.duration_background_color));
itemDurationView.setVisibility(View.VISIBLE);
} else {
itemDurationView.setVisibility(View.GONE);
}
// Default thumbnail is shown on error, while loading and if the url is empty
itemBuilder.getImageLoader().displayImage(item.thumbnail_url, itemThumbnailView,
StreamPlaylistInfoItemHolder.DISPLAY_THUMBNAIL_OPTIONS);
itemView.setOnClickListener(view -> {
if (itemBuilder.getOnStreamSelectedListener() != null) {
itemBuilder.getOnStreamSelectedListener().selected(item);
}
});
itemView.setLongClickable(true);
itemView.setOnLongClickListener(view -> {
if (itemBuilder.getOnStreamSelectedListener() != null) {
itemBuilder.getOnStreamSelectedListener().held(item);
}
return true;
});
itemThumbnailView.setOnTouchListener(getOnTouchListener(item));
itemHandleView.setOnTouchListener(getOnTouchListener(item));
}
private View.OnTouchListener getOnTouchListener(final StreamInfoItem item) {
return (view, motionEvent) -> {
view.performClick();
if (itemBuilder != null &&
motionEvent.getActionMasked() == MotionEvent.ACTION_DOWN) {
itemBuilder.getOnStreamSelectedListener()
.drag(item, StreamPlaylistInfoItemHolder.this);
}
return false;
};
}
/**
* Display options for stream thumbnails
*/
private static final DisplayImageOptions DISPLAY_THUMBNAIL_OPTIONS =
new DisplayImageOptions.Builder()
.cloneFrom(BASE_DISPLAY_IMAGE_OPTIONS)
.showImageOnFail(R.drawable.dummy_thumbnail)
.showImageForEmptyUri(R.drawable.dummy_thumbnail)
.showImageOnLoading(R.drawable.dummy_thumbnail)
.build();
}

View File

@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/itemRoot"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clickable="false"

View File

@ -0,0 +1,86 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/itemRoot"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:padding="@dimen/video_item_search_padding">
<ImageView
android:id="@+id/itemThumbnailView"
android:layout_width="98dp"
android:layout_height="55dp"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true"
android:layout_marginRight="@dimen/video_item_search_image_right_margin"
android:contentDescription="@string/list_thumbnail_view_description"
android:scaleType="centerCrop"
android:src="@drawable/dummy_thumbnail"
tools:ignore="RtlHardcoded"/>
<TextView
android:id="@+id/itemDurationView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBottom="@id/itemThumbnailView"
android:layout_alignRight="@id/itemThumbnailView"
android:layout_marginBottom="@dimen/video_item_search_duration_margin"
android:layout_marginRight="@dimen/video_item_search_duration_margin"
android:background="@color/duration_background_color"
android:paddingBottom="@dimen/video_item_search_duration_vertical_padding"
android:paddingLeft="@dimen/video_item_search_duration_horizontal_padding"
android:paddingRight="@dimen/video_item_search_duration_horizontal_padding"
android:paddingTop="@dimen/video_item_search_duration_vertical_padding"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="@color/duration_text_color"
android:textSize="@dimen/video_item_search_duration_text_size"
tools:ignore="RtlHardcoded"
tools:text="1:09:10"/>
<ImageView
android:id="@+id/itemHandle"
android:layout_width="wrap_content"
android:layout_height="55dp"
android:layout_gravity="center_vertical"
android:layout_alignParentRight="true"
android:paddingLeft="@dimen/video_item_search_image_right_margin"
android:scaleType="center"
android:src="?attr/drag_handle"
android:contentDescription="@string/detail_drag_description"
tools:ignore="RtlHardcoded,RtlSymmetry"/>
<TextView
android:id="@+id/itemVideoTitleView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_toLeftOf="@id/itemHandle"
android:layout_toStartOf="@id/itemHandle"
android:layout_toRightOf="@+id/itemThumbnailView"
android:layout_toEndOf="@+id/itemThumbnailView"
android:ellipsize="end"
android:maxLines="2"
android:textAppearance="?android:attr/textAppearanceLarge"
android:textSize="@dimen/video_item_search_title_text_size"
tools:text="Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc tristique..."/>
<TextView
android:id="@+id/itemUploaderView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/itemVideoTitleView"
android:layout_toLeftOf="@id/itemHandle"
android:layout_toStartOf="@id/itemHandle"
android:layout_toRightOf="@+id/itemThumbnailView"
android:layout_toEndOf="@+id/itemThumbnailView"
android:lines="1"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textSize="@dimen/video_item_search_uploader_text_size"
tools:text="Uploader" />
</RelativeLayout>

View File

@ -188,6 +188,7 @@
<string name="search_no_results">No results</string>
<string name="empty_view_no_videos" translatable="false">@string/no_videos</string>
<string name="empty_subscription_feed_subtitle">Nothing Here But Crickets</string>
<string name="detail_drag_description">Drag to reorder</string>
<string name="err_dir_create">Cannot create download directory \'%1$s\'</string>
<string name="info_dir_created">Created download directory \'%1$s\'</string>