-Revamped local items to display more information such as service name, etc.

-Enabled reordering, renaming, removing of items on playlist fragment.
-Enabled removal of dangling streams entries when history is cleared.
-Changed playlist append menu item to icon on service player activity.
-Added adapter and builder for local items, removed dependency on infoitem and existing infolist for database entry items.
-Removed watch history entity and DAO.
-Extracted info item selected listener to remove adding boilerplate code when long click functionality is optional.
-Fixed query returning no record on left join when right table is empty.
This commit is contained in:
John Zhen Mo 2018-01-27 22:14:38 -08:00
parent 17d77aa31f
commit 84c5d27416
38 changed files with 1224 additions and 506 deletions

View File

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

View File

@ -1,37 +0,0 @@
package org.schabi.newpipe.database.history.dao;
import android.arch.persistence.room.Dao;
import android.arch.persistence.room.Query;
import org.schabi.newpipe.database.history.model.WatchHistoryEntry;
import java.util.List;
import io.reactivex.Flowable;
import static org.schabi.newpipe.database.history.model.WatchHistoryEntry.CREATION_DATE;
import static org.schabi.newpipe.database.history.model.WatchHistoryEntry.ID;
import static org.schabi.newpipe.database.history.model.WatchHistoryEntry.SERVICE_ID;
import static org.schabi.newpipe.database.history.model.WatchHistoryEntry.TABLE_NAME;
@Dao
public interface WatchHistoryDAO extends HistoryDAO<WatchHistoryEntry> {
String ORDER_BY_CREATION_DATE = " ORDER BY " + CREATION_DATE + " DESC";
@Query("SELECT * FROM " + TABLE_NAME + " WHERE " + ID + " = (SELECT MAX(" + ID + ") FROM " + TABLE_NAME + ")")
@Override
WatchHistoryEntry getLatestEntry();
@Query("DELETE FROM " + TABLE_NAME)
@Override
int deleteAll();
@Query("SELECT * FROM " + TABLE_NAME + ORDER_BY_CREATION_DATE)
@Override
Flowable<List<WatchHistoryEntry>> getAll();
@Query("SELECT * FROM " + TABLE_NAME + " WHERE " + SERVICE_ID + " = :serviceId" + ORDER_BY_CREATION_DATE)
@Override
Flowable<List<WatchHistoryEntry>> listByService(int serviceId);
}

View File

@ -1,109 +0,0 @@
package org.schabi.newpipe.database.history.model;
import android.arch.persistence.room.ColumnInfo;
import android.arch.persistence.room.Entity;
import android.arch.persistence.room.Ignore;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import java.util.Date;
@Entity(tableName = WatchHistoryEntry.TABLE_NAME)
public class WatchHistoryEntry extends HistoryEntry {
public static final String TABLE_NAME = "watch_history";
public static final String TITLE = "title";
public static final String URL = "url";
public static final String STREAM_ID = "stream_id";
public static final String THUMBNAIL_URL = "thumbnail_url";
public static final String UPLOADER = "uploader";
public static final String DURATION = "duration";
@ColumnInfo(name = TITLE)
private String title;
@ColumnInfo(name = URL)
private String url;
@ColumnInfo(name = STREAM_ID)
private String streamId;
@ColumnInfo(name = THUMBNAIL_URL)
private String thumbnailURL;
@ColumnInfo(name = UPLOADER)
private String uploader;
@ColumnInfo(name = DURATION)
private long duration;
public WatchHistoryEntry(Date creationDate, int serviceId, String title, String url, String streamId, String thumbnailURL, String uploader, long duration) {
super(creationDate, serviceId);
this.title = title;
this.url = url;
this.streamId = streamId;
this.thumbnailURL = thumbnailURL;
this.uploader = uploader;
this.duration = duration;
}
public WatchHistoryEntry(StreamInfo streamInfo) {
this(new Date(), streamInfo.getServiceId(), streamInfo.getName(), streamInfo.getUrl(),
streamInfo.id, streamInfo.thumbnail_url, streamInfo.uploader_name, streamInfo.duration);
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getStreamId() {
return streamId;
}
public void setStreamId(String streamId) {
this.streamId = streamId;
}
public String getThumbnailURL() {
return thumbnailURL;
}
public void setThumbnailURL(String thumbnailURL) {
this.thumbnailURL = thumbnailURL;
}
public String getUploader() {
return uploader;
}
public void setUploader(String uploader) {
this.uploader = uploader;
}
public long getDuration() {
return duration;
}
public void setDuration(int duration) {
this.duration = duration;
}
@Ignore
@Override
public boolean hasEqualValues(HistoryEntry otherEntry) {
return otherEntry instanceof WatchHistoryEntry && super.hasEqualValues(otherEntry)
&& getUrl().equals(((WatchHistoryEntry) otherEntry).getUrl());
}
}

View File

@ -2,13 +2,14 @@ package org.schabi.newpipe.database.playlist;
import android.arch.persistence.room.ColumnInfo;
import org.schabi.newpipe.database.LocalItem;
import org.schabi.newpipe.info_list.stored.LocalPlaylistInfoItem;
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 {
public class PlaylistMetadataEntry implements LocalItem {
final public static String PLAYLIST_STREAM_COUNT = "streamCount";
@ColumnInfo(name = PLAYLIST_ID)
@ -33,4 +34,9 @@ public class PlaylistMetadataEntry {
storedPlaylistInfoItem.setStreamCount(streamCount);
return storedPlaylistInfoItem;
}
@Override
public LocalItemType getLocalItemType() {
return LocalItemType.PLAYLIST_ITEM;
}
}

View File

@ -0,0 +1,60 @@
package org.schabi.newpipe.database.playlist;
import android.arch.persistence.room.ColumnInfo;
import org.schabi.newpipe.database.LocalItem;
import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity;
import org.schabi.newpipe.database.stream.model.StreamEntity;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.extractor.stream.StreamType;
public class PlaylistStreamEntry implements LocalItem {
@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 = PlaylistStreamEntity.JOIN_STREAM_ID)
final public long streamId;
@ColumnInfo(name = PlaylistStreamEntity.JOIN_INDEX)
final public int joinIndex;
public PlaylistStreamEntry(long uid, int serviceId, String url, String title,
StreamType streamType, long duration, String uploader,
String thumbnailUrl, long streamId, int joinIndex) {
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.joinIndex = joinIndex;
}
public StreamInfoItem toStreamInfoItem() throws IllegalArgumentException {
StreamInfoItem item = new StreamInfoItem(serviceId, url, title, streamType);
item.setThumbnailUrl(thumbnailUrl);
item.setUploaderName(uploader);
item.setDuration(duration);
return item;
}
@Override
public LocalItemType getLocalItemType() {
return LocalItemType.PLAYLIST_STREAM_ITEM;
}
}

View File

@ -6,6 +6,7 @@ import android.arch.persistence.room.Transaction;
import org.schabi.newpipe.database.BasicDAO;
import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry;
import org.schabi.newpipe.database.playlist.PlaylistStreamEntry;
import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity;
import org.schabi.newpipe.database.stream.model.StreamEntity;
@ -37,17 +38,13 @@ public abstract class PlaylistStreamDAO implements BasicDAO<PlaylistStreamEntity
" WHERE " + JOIN_PLAYLIST_ID + " = :playlistId")
public abstract void deleteBatch(final long playlistId);
@Query("SELECT MAX(" + JOIN_INDEX + ")" +
@Query("SELECT COALESCE(MAX(" + JOIN_INDEX + "), -1)" +
" FROM " + PLAYLIST_STREAM_JOIN_TABLE +
" WHERE " + JOIN_PLAYLIST_ID + " = :playlistId")
public abstract Flowable<Integer> getMaximumIndexOf(final long playlistId);
@Transaction
@Query("SELECT " + STREAM_ID + ", " + STREAM_SERVICE_ID + ", " + STREAM_URL + ", " +
STREAM_TITLE + ", " + STREAM_TYPE + ", " + STREAM_UPLOADER + ", " +
STREAM_DURATION + ", " + STREAM_THUMBNAIL_URL +
" FROM " + STREAM_TABLE + " INNER JOIN " +
@Query("SELECT * FROM " + STREAM_TABLE + " INNER JOIN " +
// get ids of streams of the given playlist
"(SELECT " + JOIN_STREAM_ID + "," + JOIN_INDEX +
" FROM " + PLAYLIST_STREAM_JOIN_TABLE + " WHERE "
@ -56,14 +53,16 @@ public abstract class PlaylistStreamDAO implements BasicDAO<PlaylistStreamEntity
// then merge with the stream metadata
" ON " + STREAM_ID + " = " + JOIN_STREAM_ID +
" ORDER BY " + JOIN_INDEX + " ASC")
public abstract Flowable<List<StreamEntity>> getOrderedStreamsOf(long playlistId);
public abstract Flowable<List<PlaylistStreamEntry>> getOrderedStreamsOf(long playlistId);
@Transaction
@Query("SELECT " + PLAYLIST_ID + ", " + PLAYLIST_NAME + ", " +
PLAYLIST_THUMBNAIL_URL + ", COUNT(*) AS " + PLAYLIST_STREAM_COUNT +
PLAYLIST_THUMBNAIL_URL + ", " +
"COALESCE(COUNT(" + JOIN_PLAYLIST_ID + "), 0) AS " + PLAYLIST_STREAM_COUNT +
" FROM " + PLAYLIST_TABLE + " LEFT JOIN " + PLAYLIST_STREAM_JOIN_TABLE +
" ON " + PLAYLIST_TABLE + "." + PLAYLIST_ID + " = " + PLAYLIST_STREAM_JOIN_TABLE + "." + JOIN_PLAYLIST_ID +
" FROM " + PLAYLIST_TABLE +
" LEFT JOIN " + PLAYLIST_STREAM_JOIN_TABLE +
" ON " + PLAYLIST_ID + " = " + JOIN_PLAYLIST_ID +
" GROUP BY " + JOIN_PLAYLIST_ID)
public abstract Flowable<List<PlaylistMetadataEntry>> getPlaylistMetadata();
}

View File

@ -2,14 +2,15 @@ package org.schabi.newpipe.database.stream;
import android.arch.persistence.room.ColumnInfo;
import org.schabi.newpipe.database.LocalItem;
import org.schabi.newpipe.database.history.model.StreamHistoryEntity;
import org.schabi.newpipe.database.stream.model.StreamEntity;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.extractor.stream.StreamType;
import org.schabi.newpipe.info_list.stored.StreamStatisticsInfoItem;
import java.util.Date;
public class StreamStatisticsEntry {
public class StreamStatisticsEntry implements LocalItem {
final public static String STREAM_LATEST_DATE = "latestAccess";
final public static String STREAM_WATCH_COUNT = "watchCount";
@ -53,14 +54,16 @@ public class StreamStatisticsEntry {
this.watchCount = watchCount;
}
public StreamStatisticsInfoItem toStreamStatisticsInfoItem() {
StreamStatisticsInfoItem item =
new StreamStatisticsInfoItem(uid, serviceId, url, title, streamType);
public StreamInfoItem toStreamInfoItem() {
StreamInfoItem item = new StreamInfoItem(serviceId, url, title, streamType);
item.setDuration(duration);
item.setUploaderName(uploader);
item.setThumbnailUrl(thumbnailUrl);
item.setLatestAccessDate(latestAccessDate);
item.setWatchCount(watchCount);
return item;
}
@Override
public LocalItemType getLocalItemType() {
return LocalItemType.STATISTIC_STREAM_ITEM;
}
}

View File

@ -9,7 +9,6 @@ import android.arch.persistence.room.PrimaryKey;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.extractor.stream.StreamType;
import org.schabi.newpipe.info_list.stored.StreamEntityInfoItem;
import org.schabi.newpipe.playlist.PlayQueueItem;
import org.schabi.newpipe.util.Constants;
@ -88,16 +87,6 @@ public class StreamEntity implements Serializable {
item.getThumbnailUrl(), item.getUploader(), item.getDuration());
}
@Ignore
public StreamEntityInfoItem toStreamEntityInfoItem() throws IllegalArgumentException {
StreamEntityInfoItem item = new StreamEntityInfoItem(getUid(), getServiceId(),
getUrl(), getTitle(), getStreamType());
item.setThumbnailUrl(getThumbnailUrl());
item.setUploaderName(getUploader());
item.setDuration(getDuration());
return item;
}
public long getUid() {
return uid;
}

View File

@ -29,7 +29,7 @@ import org.schabi.newpipe.extractor.kiosk.KioskList;
import org.schabi.newpipe.fragments.list.channel.ChannelFragment;
import org.schabi.newpipe.fragments.list.feed.FeedFragment;
import org.schabi.newpipe.fragments.list.kiosk.KioskFragment;
import org.schabi.newpipe.fragments.local.BookmarkFragment;
import org.schabi.newpipe.fragments.local.bookmark.BookmarkFragment;
import org.schabi.newpipe.fragments.subscription.SubscriptionFragment;
import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.report.UserAction;

View File

@ -0,0 +1,184 @@
package org.schabi.newpipe.fragments.local;
import android.content.Context;
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.Menu;
import android.view.MenuInflater;
import android.view.View;
import org.schabi.newpipe.R;
import org.schabi.newpipe.database.LocalItem;
import org.schabi.newpipe.fragments.BaseStateFragment;
import org.schabi.newpipe.fragments.list.ListViewContract;
import org.schabi.newpipe.util.StateSaver;
import java.util.List;
import java.util.Queue;
import static org.schabi.newpipe.util.AnimationUtils.animateView;
public abstract class BaseLocalListFragment<I, N> extends BaseStateFragment<I>
implements ListViewContract<I, N>, StateSaver.WriteRead {
/*//////////////////////////////////////////////////////////////////////////
// Views
//////////////////////////////////////////////////////////////////////////*/
protected LocalItemListAdapter itemListAdapter;
protected RecyclerView itemsList;
/*//////////////////////////////////////////////////////////////////////////
// LifeCycle
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onAttach(Context context) {
super.onAttach(context);
itemListAdapter = new LocalItemListAdapter(activity);
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setHasOptionsMenu(true);
}
@Override
public void onDestroy() {
super.onDestroy();
StateSaver.onDestroy(savedState);
}
/*//////////////////////////////////////////////////////////////////////////
// State Saving
//////////////////////////////////////////////////////////////////////////*/
protected StateSaver.SavedState savedState;
@Override
public String generateSuffix() {
// Naive solution, but it's good for now (the items don't change)
return "." + itemListAdapter.getItemsList().size() + ".list";
}
@Override
public void writeTo(Queue<Object> objectsToSave) {
objectsToSave.add(itemListAdapter.getItemsList());
}
@Override
@SuppressWarnings("unchecked")
public void readFrom(@NonNull Queue<Object> savedObjects) throws Exception {
itemListAdapter.getItemsList().clear();
itemListAdapter.getItemsList().addAll((List<LocalItem>) savedObjects.poll());
}
@Override
public void onSaveInstanceState(Bundle bundle) {
super.onSaveInstanceState(bundle);
savedState = StateSaver.tryToSave(activity.isChangingConfigurations(), savedState, bundle, this);
}
@Override
protected void onRestoreInstanceState(@NonNull Bundle bundle) {
super.onRestoreInstanceState(bundle);
savedState = StateSaver.tryToRestore(bundle, this);
}
/*//////////////////////////////////////////////////////////////////////////
// Init
//////////////////////////////////////////////////////////////////////////*/
protected View getListHeader() {
return null;
}
protected View getListFooter() {
return activity.getLayoutInflater().inflate(R.layout.pignate_footer, itemsList, false);
}
protected RecyclerView.LayoutManager getListLayoutManager() {
return new LinearLayoutManager(activity);
}
@Override
protected void initViews(View rootView, Bundle savedInstanceState) {
super.initViews(rootView, savedInstanceState);
itemsList = rootView.findViewById(R.id.items_list);
itemsList.setLayoutManager(getListLayoutManager());
itemListAdapter.setFooter(getListFooter());
itemListAdapter.setHeader(getListHeader());
itemsList.setAdapter(itemListAdapter);
}
@Override
protected void initListeners() {
super.initListeners();
}
/*//////////////////////////////////////////////////////////////////////////
// Menu
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
if (DEBUG) Log.d(TAG, "onCreateOptionsMenu() called with: menu = [" + menu + "], inflater = [" + inflater + "]");
super.onCreateOptionsMenu(menu, inflater);
ActionBar supportActionBar = activity.getSupportActionBar();
if (supportActionBar != null) {
supportActionBar.setDisplayShowTitleEnabled(true);
if(useAsFrontPage) {
supportActionBar.setDisplayHomeAsUpEnabled(false);
} else {
supportActionBar.setDisplayHomeAsUpEnabled(true);
}
}
}
/*//////////////////////////////////////////////////////////////////////////
// Contract
//////////////////////////////////////////////////////////////////////////*/
@Override
public void showLoading() {
super.showLoading();
// animateView(itemsList, false, 400);
}
@Override
public void hideLoading() {
super.hideLoading();
animateView(itemsList, true, 300);
}
@Override
public void showError(String message, boolean showRetryButton) {
super.showError(message, showRetryButton);
showListFooter(false);
animateView(itemsList, false, 200);
}
@Override
public void showEmptyState() {
super.showEmptyState();
showListFooter(false);
}
@Override
public void showListFooter(final boolean show) {
itemsList.post(() -> itemListAdapter.showFooter(show));
}
@Override
public void handleNextItems(N result) {
isLoading.set(false);
}
}

View File

@ -0,0 +1,13 @@
package org.schabi.newpipe.fragments.local;
import android.support.v7.widget.RecyclerView;
import android.view.View;
public class HeaderFooterHolder extends RecyclerView.ViewHolder {
public View view;
public HeaderFooterHolder(View v) {
super(v);
view = v;
}
}

View File

@ -0,0 +1,56 @@
package org.schabi.newpipe.fragments.local;
import android.content.Context;
import com.nostra13.universalimageloader.core.ImageLoader;
import org.schabi.newpipe.database.LocalItem;
/*
* Created by Christian Schabesberger on 26.09.16.
* <p>
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
* InfoItemBuilder.java is part of NewPipe.
* <p>
* NewPipe is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* <p>
* NewPipe is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
* <p>
* You should have received a copy of the GNU General Public License
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
public class LocalItemBuilder {
private static final String TAG = LocalItemBuilder.class.toString();
private final Context context;
private ImageLoader imageLoader = ImageLoader.getInstance();
private OnCustomItemGesture<LocalItem> onSelectedListener;
public LocalItemBuilder(Context context) {
this.context = context;
}
public Context getContext() {
return context;
}
public ImageLoader getImageLoader() {
return imageLoader;
}
public OnCustomItemGesture<LocalItem> getOnItemSelectedListener() {
return onSelectedListener;
}
public void setOnItemSelectedListener(OnCustomItemGesture<LocalItem> listener) {
this.onSelectedListener = listener;
}
}

View File

@ -0,0 +1,243 @@
package org.schabi.newpipe.fragments.local;
import android.app.Activity;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import org.schabi.newpipe.database.LocalItem;
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.util.Localization;
import java.text.DateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/*
* Created by Christian Schabesberger on 01.08.16.
*
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
* InfoListAdapter.java is part of NewPipe.
*
* NewPipe is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* NewPipe is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private static final String TAG = LocalItemListAdapter.class.getSimpleName();
private static final boolean DEBUG = false;
private static final int HEADER_TYPE = 0;
private static final int FOOTER_TYPE = 1;
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 final LocalItemBuilder localItemBuilder;
private final ArrayList<LocalItem> localItems;
private final DateFormat dateFormat;
private boolean showFooter = false;
private View header = null;
private View footer = null;
public LocalItemListAdapter(Activity activity) {
localItemBuilder = new LocalItemBuilder(activity);
localItems = new ArrayList<>();
dateFormat = DateFormat.getDateInstance(DateFormat.SHORT,
Localization.getPreferredLocale(activity));
}
public void setSelectedListener(OnCustomItemGesture<LocalItem> listener) {
localItemBuilder.setOnItemSelectedListener(listener);
}
public void addInfoItemList(List<? extends LocalItem> data) {
if (data != null) {
if (DEBUG) {
Log.d(TAG, "addInfoItemList() before > localItems.size() = " +
localItems.size() + ", data.size() = " + data.size());
}
int offsetStart = sizeConsideringHeader();
localItems.addAll(data);
if (DEBUG) {
Log.d(TAG, "addInfoItemList() after > offsetStart = " + offsetStart +
", localItems.size() = " + localItems.size() +
", header = " + header + ", footer = " + footer +
", showFooter = " + showFooter);
}
notifyItemRangeInserted(offsetStart, data.size());
if (footer != null && showFooter) {
int footerNow = sizeConsideringHeader();
notifyItemMoved(offsetStart, footerNow);
if (DEBUG) Log.d(TAG, "addInfoItemList() footer from " + offsetStart +
" to " + footerNow);
}
}
}
public void addInfoItem(LocalItem data) {
addInfoItemList(Collections.singletonList(data));
}
public void removeItemAt(final int infoListPosition) {
if (infoListPosition < 0 || infoListPosition >= localItems.size()) return;
localItems.remove(infoListPosition);
notifyItemRemoved(infoListPosition + (header != null ? 1 : 0));
}
public boolean swapItems(int fromAdapterPosition, int toAdapterPosition) {
final int actualFrom = offsetWithoutHeader(fromAdapterPosition);
final int actualTo = offsetWithoutHeader(toAdapterPosition);
if (actualFrom < 0 || actualTo < 0) return false;
if (actualFrom >= localItems.size() || actualTo >= localItems.size()) return false;
localItems.add(actualTo, localItems.remove(actualFrom));
notifyItemMoved(fromAdapterPosition, toAdapterPosition);
return true;
}
public void clearStreamItemList() {
if (localItems.isEmpty()) {
return;
}
localItems.clear();
notifyDataSetChanged();
}
public void setHeader(View header) {
boolean changed = header != this.header;
this.header = header;
if (changed) notifyDataSetChanged();
}
public void setFooter(View view) {
this.footer = view;
}
public void showFooter(boolean show) {
if (DEBUG) Log.d(TAG, "showFooter() called with: show = [" + show + "]");
if (show == showFooter) return;
showFooter = show;
if (show) notifyItemInserted(sizeConsideringHeader());
else notifyItemRemoved(sizeConsideringHeader());
}
private int offsetWithoutHeader(final int offset) {
return offset - (header != null ? 1 : 0);
}
private int sizeConsideringHeader() {
return localItems.size() + (header != null ? 1 : 0);
}
public ArrayList<LocalItem> getItemsList() {
return localItems;
}
@Override
public int getItemCount() {
int count = localItems.size();
if (header != null) count++;
if (footer != null && showFooter) count++;
if (DEBUG) {
Log.d(TAG, "getItemCount() called, count = " + count +
", localItems.size() = " + localItems.size() +
", header = " + header + ", footer = " + footer +
", showFooter = " + showFooter);
}
return count;
}
@Override
public int getItemViewType(int position) {
if (DEBUG) Log.d(TAG, "getItemViewType() called with: position = [" + position + "]");
if (header != null && position == 0) {
return HEADER_TYPE;
} else if (header != null) {
position--;
}
if (footer != null && position == localItems.size() && showFooter) {
return FOOTER_TYPE;
}
final LocalItem item = localItems.get(position);
switch (item.getLocalItemType()) {
case PLAYLIST_ITEM: return PLAYLIST_HOLDER_TYPE;
case PLAYLIST_STREAM_ITEM: return STREAM_PLAYLIST_HOLDER_TYPE;
case STATISTIC_STREAM_ITEM: return STREAM_STATISTICS_HOLDER_TYPE;
default:
Log.e(TAG, "No holder type has been considered for item: [" +
item.getLocalItemType() + "]");
return -1;
}
}
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int type) {
if (DEBUG) Log.d(TAG, "onCreateViewHolder() called with: parent = [" +
parent + "], type = [" + type + "]");
switch (type) {
case HEADER_TYPE:
return new HeaderFooterHolder(header);
case FOOTER_TYPE:
return new HeaderFooterHolder(footer);
case PLAYLIST_HOLDER_TYPE:
return new LocalPlaylistItemHolder(localItemBuilder, parent);
case STREAM_PLAYLIST_HOLDER_TYPE:
return new LocalPlaylistStreamItemHolder(localItemBuilder, parent);
case STREAM_STATISTICS_HOLDER_TYPE:
return new LocalStatisticStreamItemHolder(localItemBuilder, parent);
default:
Log.e(TAG, "No view type has been considered for holder: [" + type + "]");
return null;
}
}
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
if (DEBUG) Log.d(TAG, "onBindViewHolder() called with: holder = [" +
holder.getClass().getSimpleName() + "], position = [" + position + "]");
if (holder instanceof LocalItemHolder) {
// If header isn't null, offset the items by -1
if (header != null) position--;
((LocalItemHolder) holder).updateFromItem(localItems.get(position), dateFormat);
} else if (holder instanceof HeaderFooterHolder && position == 0 && header != null) {
((HeaderFooterHolder) holder).view = header;
} else if (holder instanceof HeaderFooterHolder && position == sizeConsideringHeader()
&& footer != null && showFooter) {
((HeaderFooterHolder) holder).view = footer;
}
}
}

View File

@ -7,25 +7,29 @@ import android.os.Bundle;
import android.os.Parcelable;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v7.app.AlertDialog;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.helper.ItemTouchHelper;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;
import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription;
import org.schabi.newpipe.NewPipeDatabase;
import org.schabi.newpipe.R;
import org.schabi.newpipe.database.stream.model.StreamEntity;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.database.LocalItem;
import org.schabi.newpipe.database.playlist.PlaylistStreamEntry;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.fragments.list.BaseListFragment;
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;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.NavigationHelper;
import java.util.ArrayList;
@ -37,7 +41,7 @@ import io.reactivex.disposables.CompositeDisposable;
import static org.schabi.newpipe.util.AnimationUtils.animateView;
public class LocalPlaylistFragment extends BaseListFragment<List<StreamEntity>, Void> {
public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistStreamEntry>, Void> {
private View headerRootLayout;
private TextView headerTitleView;
@ -49,12 +53,14 @@ public class LocalPlaylistFragment extends BaseListFragment<List<StreamEntity>,
private View headerBackgroundButton;
@State
protected long playlistId;
protected Long playlistId;
@State
protected String name;
@State
protected Parcelable itemsListState;
private ItemTouchHelper itemTouchHelper;
/* Used for independent events */
private CompositeDisposable disposables = new CompositeDisposable();
private Subscription databaseSubscription;
@ -86,6 +92,9 @@ public class LocalPlaylistFragment extends BaseListFragment<List<StreamEntity>,
@Override
public void onPause() {
super.onPause();
saveJoin();
itemsListState = itemsList.getLayoutManager().onSaveInstanceState();
}
@ -115,8 +124,6 @@ public class LocalPlaylistFragment extends BaseListFragment<List<StreamEntity>,
@Override
protected void initViews(View rootView, Bundle savedInstanceState) {
super.initViews(rootView, savedInstanceState);
infoListAdapter.useMiniItemVariants(true);
setFragmentTitle(name);
}
@ -141,44 +148,61 @@ public class LocalPlaylistFragment extends BaseListFragment<List<StreamEntity>,
protected void initListeners() {
super.initListeners();
infoListAdapter.setOnStreamSelectedListener(new OnInfoItemGesture<StreamInfoItem>() {
itemTouchHelper = new ItemTouchHelper(getItemTouchCallback());
itemTouchHelper.attachToRecyclerView(itemsList);
itemListAdapter.setSelectedListener(new OnCustomItemGesture<LocalItem>() {
@Override
public void selected(StreamInfoItem selectedItem) {
// Requires the parent fragment to find holder for fragment replacement
NavigationHelper.openVideoDetailFragment(getFragmentManager(),
selectedItem.getServiceId(), selectedItem.url, selectedItem.getName());
public void selected(LocalItem selectedItem) {
if (selectedItem instanceof PlaylistStreamEntry) {
final PlaylistStreamEntry item = (PlaylistStreamEntry) selectedItem;
NavigationHelper.openVideoDetailFragment(getFragmentManager(),
item.serviceId, item.url, item.title);
}
}
@Override
public void held(StreamInfoItem selectedItem) {
showStreamDialog(selectedItem);
public void held(LocalItem selectedItem) {
if (selectedItem instanceof PlaylistStreamEntry) {
showStreamDialog((PlaylistStreamEntry) selectedItem);
}
}
@Override
public void drag(LocalItem selectedItem, RecyclerView.ViewHolder viewHolder) {
if (itemTouchHelper != null) itemTouchHelper.startDrag(viewHolder);
}
});
headerTitleView.setOnClickListener(view -> createRenameDialog());
}
@Override
protected void showStreamDialog(final StreamInfoItem item) {
protected void showStreamDialog(final PlaylistStreamEntry item) {
final Context context = getContext();
final Activity activity = getActivity();
if (context == null || context.getResources() == null || getActivity() == null) return;
final StreamInfoItem infoItem = item.toStreamInfoItem();
final String[] commands = new String[]{
context.getResources().getString(R.string.enqueue_on_background),
context.getResources().getString(R.string.enqueue_on_popup),
context.getResources().getString(R.string.start_here_on_main),
context.getResources().getString(R.string.start_here_on_background),
context.getResources().getString(R.string.start_here_on_popup),
"Set as Thumbnail",
context.getResources().getString(R.string.delete)
};
final DialogInterface.OnClickListener actions = (dialogInterface, i) -> {
final int index = Math.max(infoListAdapter.getItemsList().indexOf(item), 0);
final int index = Math.max(itemListAdapter.getItemsList().indexOf(item), 0);
switch (i) {
case 0:
NavigationHelper.enqueueOnBackgroundPlayer(context, new SinglePlayQueue(item));
NavigationHelper.enqueueOnBackgroundPlayer(context,
new SinglePlayQueue(infoItem));
break;
case 1:
NavigationHelper.enqueueOnPopupPlayer(activity, new SinglePlayQueue(item));
NavigationHelper.enqueueOnPopupPlayer(activity, new
SinglePlayQueue(infoItem));
break;
case 2:
NavigationHelper.playOnMainPlayer(context, getPlayQueue(index));
@ -189,18 +213,56 @@ public class LocalPlaylistFragment extends BaseListFragment<List<StreamEntity>,
case 4:
NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(index));
break;
case 5:
changeThumbnailUrl(item.thumbnailUrl);
break;
case 6:
itemListAdapter.removeItemAt(index);
setVideoCount(itemListAdapter.getItemsList().size());
break;
default:
break;
}
};
new InfoItemDialog(getActivity(), item, commands, actions).show();
new InfoItemDialog(getActivity(), infoItem, commands, actions).show();
}
private ItemTouchHelper.SimpleCallback getItemTouchCallback() {
return new ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP | ItemTouchHelper.DOWN,
ItemTouchHelper.ACTION_STATE_IDLE) {
@Override
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder source,
RecyclerView.ViewHolder target) {
if (source.getItemViewType() != target.getItemViewType() ||
itemListAdapter == null) {
return false;
}
final int sourceIndex = source.getAdapterPosition();
final int targetIndex = target.getAdapterPosition();
return itemListAdapter.swapItems(sourceIndex, targetIndex);
}
@Override
public boolean isLongPressDragEnabled() {
return false;
}
@Override
public boolean isItemViewSwipeEnabled() {
return false;
}
@Override
public void onSwiped(RecyclerView.ViewHolder viewHolder, int swipeDir) {}
};
}
private void resetFragment() {
if (disposables != null) disposables.clear();
if (databaseSubscription != null) databaseSubscription.cancel();
if (infoListAdapter != null) infoListAdapter.clearStreamItemList();
if (itemListAdapter != null) itemListAdapter.clearStreamItemList();
}
///////////////////////////////////////////////////////////////////////////
@ -224,8 +286,8 @@ public class LocalPlaylistFragment extends BaseListFragment<List<StreamEntity>,
.subscribe(getPlaylistObserver());
}
private Subscriber<List<StreamEntity>> getPlaylistObserver() {
return new Subscriber<List<StreamEntity>>() {
private Subscriber<List<PlaylistStreamEntry>> getPlaylistObserver() {
return new Subscriber<List<PlaylistStreamEntry>>() {
@Override
public void onSubscribe(Subscription s) {
showLoading();
@ -236,7 +298,7 @@ public class LocalPlaylistFragment extends BaseListFragment<List<StreamEntity>,
}
@Override
public void onNext(List<StreamEntity> streams) {
public void onNext(List<PlaylistStreamEntry> streams) {
handleResult(streams);
if (databaseSubscription != null) databaseSubscription.request(1);
}
@ -253,9 +315,9 @@ public class LocalPlaylistFragment extends BaseListFragment<List<StreamEntity>,
}
@Override
public void handleResult(@NonNull List<StreamEntity> result) {
public void handleResult(@NonNull List<PlaylistStreamEntry> result) {
super.handleResult(result);
infoListAdapter.clearStreamItemList();
itemListAdapter.clearStreamItemList();
if (result.isEmpty()) {
showEmptyState();
@ -265,15 +327,14 @@ public class LocalPlaylistFragment extends BaseListFragment<List<StreamEntity>,
animateView(headerRootLayout, true, 100);
animateView(itemsList, true, 300);
infoListAdapter.addInfoItemList(getStreamItems(result));
itemListAdapter.addInfoItemList(result);
if (itemsListState != null) {
itemsList.getLayoutManager().onRestoreInstanceState(itemsListState);
itemsListState = null;
}
setVideoCount(itemListAdapter.getItemsList().size());
playlistControl.setVisibility(View.VISIBLE);
headerStreamCount.setText(
getResources().getQuantityString(R.plurals.videos, result.size(), result.size()));
headerPlayAllButton.setOnClickListener(view ->
NavigationHelper.playOnMainPlayer(activity, getPlayQueue()));
@ -284,29 +345,6 @@ public class LocalPlaylistFragment extends BaseListFragment<List<StreamEntity>,
hideLoading();
}
private List<InfoItem> getStreamItems(final List<StreamEntity> streams) {
List<InfoItem> items = new ArrayList<>(streams.size());
for (final StreamEntity stream : streams) {
items.add(stream.toStreamEntityInfoItem());
}
return items;
}
/*//////////////////////////////////////////////////////////////////////////
// Contract
//////////////////////////////////////////////////////////////////////////*/
@Override
protected void loadMoreItems() {
// Do nothing
}
@Override
protected boolean hasMoreItems() {
return false;
}
///////////////////////////////////////////////////////////////////////////
// Fragment Error Handling
///////////////////////////////////////////////////////////////////////////
@ -325,13 +363,13 @@ public class LocalPlaylistFragment extends BaseListFragment<List<StreamEntity>,
// Utils
//////////////////////////////////////////////////////////////////////////*/
protected void setInitialData(long playlistId, String name) {
private void setInitialData(long playlistId, String name) {
this.playlistId = playlistId;
this.name = !TextUtils.isEmpty(name) ? name : "";
}
protected void setFragmentTitle(final String title) {
if (activity.getSupportActionBar() != null) {
private void setFragmentTitle(final String title) {
if (activity != null && activity.getSupportActionBar() != null) {
activity.getSupportActionBar().setTitle(title);
}
if (headerTitleView != null) {
@ -339,17 +377,80 @@ public class LocalPlaylistFragment extends BaseListFragment<List<StreamEntity>,
}
}
private void setVideoCount(final long count) {
if (activity != null && headerStreamCount != null) {
headerStreamCount.setText(Localization.localizeStreamCount(activity, count));
}
}
private PlayQueue getPlayQueue() {
return getPlayQueue(0);
}
private PlayQueue getPlayQueue(final int index) {
final List<InfoItem> infoItems = infoListAdapter.getItemsList();
final List<LocalItem> infoItems = itemListAdapter.getItemsList();
List<StreamInfoItem> streamInfoItems = new ArrayList<>(infoItems.size());
for (final InfoItem item : infoItems) {
if (item instanceof StreamInfoItem) streamInfoItems.add((StreamInfoItem) item);
for (final LocalItem item : infoItems) {
if (item instanceof PlaylistStreamEntry) {
streamInfoItems.add(((PlaylistStreamEntry) item).toStreamInfoItem());
}
}
return new SinglePlayQueue(streamInfoItems, index);
}
private void createRenameDialog() {
if (playlistId == null || name == null || getContext() == null) return;
final View dialogView = View.inflate(getContext(), R.layout.dialog_playlist_name, null);
EditText nameEdit = dialogView.findViewById(R.id.playlist_name);
nameEdit.setText(name);
nameEdit.setSelection(nameEdit.getText().length());
final AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(getContext())
.setTitle(R.string.rename_playlist)
.setView(dialogView)
.setCancelable(true)
.setNegativeButton(R.string.cancel, null)
.setPositiveButton(R.string.create, (dialogInterface, i) -> {
name = nameEdit.getText().toString();
setFragmentTitle(name);
final LocalPlaylistManager playlistManager =
new LocalPlaylistManager(NewPipeDatabase.getInstance(getContext()));
final Toast successToast = Toast.makeText(getActivity(),
"Playlist renamed",
Toast.LENGTH_SHORT);
playlistManager.renamePlaylist(playlistId, name)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(longs -> successToast.show());
});
dialogBuilder.show();
}
private void changeThumbnailUrl(final String thumbnailUrl) {
final Toast successToast = Toast.makeText(getActivity(),
"Playlist thumbnail changed",
Toast.LENGTH_SHORT);
playlistManager.changePlaylistThumbnail(playlistId, thumbnailUrl)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(ignore -> successToast.show());
}
private void saveJoin() {
final List<LocalItem> items = itemListAdapter.getItemsList();
List<Long> streamIds = new ArrayList<>(items.size());
for (final LocalItem item : items) {
if (item instanceof PlaylistStreamEntry) {
streamIds.add(((PlaylistStreamEntry) item).streamId);
}
}
playlistManager.updateJoin(playlistId, streamIds)
.observeOn(AndroidSchedulers.mainThread())
.subscribe();
}
}

View File

@ -4,6 +4,7 @@ import android.support.annotation.Nullable;
import org.schabi.newpipe.database.AppDatabase;
import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry;
import org.schabi.newpipe.database.playlist.PlaylistStreamEntry;
import org.schabi.newpipe.database.playlist.dao.PlaylistDAO;
import org.schabi.newpipe.database.playlist.dao.PlaylistStreamDAO;
import org.schabi.newpipe.database.playlist.model.PlaylistEntity;
@ -84,7 +85,7 @@ public class LocalPlaylistManager {
return playlistStreamTable.getPlaylistMetadata().subscribeOn(Schedulers.io());
}
public Flowable<List<StreamEntity>> getPlaylistStreams(final long playlistId) {
public Flowable<List<PlaylistStreamEntry>> getPlaylistStreams(final long playlistId) {
return playlistStreamTable.getOrderedStreamsOf(playlistId).subscribeOn(Schedulers.io());
}

View File

@ -1,35 +0,0 @@
package org.schabi.newpipe.fragments.local;
import org.schabi.newpipe.R;
import org.schabi.newpipe.database.stream.StreamStatisticsEntry;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.info_list.stored.StreamStatisticsInfoItem;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class MostPlayedFragment extends StatisticsPlaylistFragment {
@Override
protected String getName() {
return getString(R.string.title_most_played);
}
@Override
protected List<InfoItem> processResult(List<StreamStatisticsEntry> results) {
Collections.sort(results, (left, right) ->
((Long) right.watchCount).compareTo(left.watchCount));
List<InfoItem> items = new ArrayList<>(results.size());
for (final StreamStatisticsEntry stream : results) {
items.add(stream.toStreamStatisticsInfoItem());
}
return items;
}
@Override
protected String getAdditionalDetail(StreamStatisticsInfoItem infoItem) {
final int watchCount = (int) infoItem.getWatchCount();
return getResources().getQuantityString(R.plurals.views, watchCount, watchCount);
}
}

View File

@ -0,0 +1,19 @@
package org.schabi.newpipe.fragments.local;
import android.support.v7.widget.RecyclerView;
import org.schabi.newpipe.database.LocalItem;
import org.schabi.newpipe.extractor.InfoItem;
public abstract class OnCustomItemGesture<T extends LocalItem> {
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

@ -1,36 +0,0 @@
package org.schabi.newpipe.fragments.local;
import android.text.format.DateFormat;
import org.schabi.newpipe.R;
import org.schabi.newpipe.database.stream.StreamStatisticsEntry;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.info_list.stored.StreamStatisticsInfoItem;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class WatchHistoryFragment extends StatisticsPlaylistFragment {
@Override
protected String getName() {
return getString(R.string.title_watch_history);
}
@Override
protected List<InfoItem> processResult(List<StreamStatisticsEntry> results) {
Collections.sort(results, (left, right) ->
right.latestAccessDate.compareTo(left.latestAccessDate));
List<InfoItem> items = new ArrayList<>(results.size());
for (final StreamStatisticsEntry stream : results) {
items.add(stream.toStreamStatisticsInfoItem());
}
return items;
}
@Override
protected String getAdditionalDetail(StreamStatisticsInfoItem infoItem) {
return DateFormat.getLongDateFormat(getContext()).format(infoItem.getLatestAccessDate());
}
}

View File

@ -1,7 +1,7 @@
package org.schabi.newpipe.fragments.local;
package org.schabi.newpipe.fragments.local.bookmark;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.os.Bundle;
import android.os.Parcelable;
import android.support.annotation.NonNull;
@ -17,18 +17,15 @@ import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription;
import org.schabi.newpipe.NewPipeDatabase;
import org.schabi.newpipe.R;
import org.schabi.newpipe.database.LocalItem;
import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem;
import org.schabi.newpipe.fragments.BaseStateFragment;
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.fragments.local.LocalItemListAdapter;
import org.schabi.newpipe.fragments.local.LocalPlaylistManager;
import org.schabi.newpipe.fragments.local.OnCustomItemGesture;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.NavigationHelper;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@ -42,7 +39,7 @@ public class BookmarkFragment extends BaseStateFragment<List<PlaylistMetadataEnt
private View watchHistoryButton;
private View mostWatchedButton;
private InfoListAdapter infoListAdapter;
private LocalItemListAdapter itemListAdapter;
private RecyclerView itemsList;
@State
@ -68,7 +65,7 @@ public class BookmarkFragment extends BaseStateFragment<List<PlaylistMetadataEnt
@Override
public void onAttach(Context context) {
super.onAttach(context);
infoListAdapter = new InfoListAdapter(activity);
itemListAdapter = new LocalItemListAdapter(activity);
localPlaylistManager = new LocalPlaylistManager(NewPipeDatabase.getInstance(context));
}
@ -122,7 +119,6 @@ public class BookmarkFragment extends BaseStateFragment<List<PlaylistMetadataEnt
protected void initViews(View rootView, Bundle savedInstanceState) {
super.initViews(rootView, savedInstanceState);
infoListAdapter = new InfoListAdapter(getActivity());
itemsList = rootView.findViewById(R.id.items_list);
itemsList.setLayoutManager(new LinearLayoutManager(activity));
@ -131,35 +127,30 @@ public class BookmarkFragment extends BaseStateFragment<List<PlaylistMetadataEnt
watchHistoryButton = headerRootLayout.findViewById(R.id.watchHistory);
mostWatchedButton = headerRootLayout.findViewById(R.id.mostWatched);
infoListAdapter.setHeader(headerRootLayout);
infoListAdapter.useMiniItemVariants(true);
itemListAdapter.setHeader(headerRootLayout);
itemsList.setAdapter(infoListAdapter);
itemsList.setAdapter(itemListAdapter);
}
@Override
protected void initListeners() {
super.initListeners();
infoListAdapter.setOnPlaylistSelectedListener(new OnInfoItemGesture<PlaylistInfoItem>() {
itemListAdapter.setSelectedListener(new OnCustomItemGesture<LocalItem>() {
@Override
public void selected(PlaylistInfoItem selectedItem) {
public void selected(LocalItem selectedItem) {
// Requires the parent fragment to find holder for fragment replacement
if (selectedItem instanceof LocalPlaylistInfoItem && getParentFragment() != null) {
final long playlistId = ((LocalPlaylistInfoItem) selectedItem).getPlaylistId();
if (selectedItem instanceof PlaylistMetadataEntry && getParentFragment() != null) {
final PlaylistMetadataEntry entry = ((PlaylistMetadataEntry) selectedItem);
NavigationHelper.openLocalPlaylistFragment(
getParentFragment().getFragmentManager(),
playlistId,
selectedItem.getName()
);
getParentFragment().getFragmentManager(), entry.uid, entry.name);
}
}
@Override
public void held(PlaylistInfoItem selectedItem) {
if (selectedItem instanceof LocalPlaylistInfoItem) {
showPlaylistDialog((LocalPlaylistInfoItem) selectedItem);
public void held(LocalItem selectedItem) {
if (selectedItem instanceof PlaylistMetadataEntry) {
showDeleteDialog((PlaylistMetadataEntry) selectedItem);
}
}
});
@ -177,36 +168,25 @@ public class BookmarkFragment extends BaseStateFragment<List<PlaylistMetadataEnt
});
}
private void showPlaylistDialog(final LocalPlaylistInfoItem item) {
final Context context = getContext();
if (context == null || context.getResources() == null || getActivity() == null) return;
final String[] commands = new String[]{
context.getResources().getString(R.string.delete_playlist)
};
final DialogInterface.OnClickListener actions = (dialogInterface, i) -> {
switch (i) {
case 0:
private void showDeleteDialog(final PlaylistMetadataEntry item) {
new AlertDialog.Builder(activity)
.setTitle(item.name)
.setMessage(R.string.delete_playlist_prompt)
.setCancelable(true)
.setPositiveButton(R.string.delete, (dialog, i) -> {
final Toast deleteSuccessful =
Toast.makeText(getContext(), "Deleted", Toast.LENGTH_SHORT);
disposables.add(localPlaylistManager.deletePlaylist(item.getPlaylistId())
disposables.add(localPlaylistManager.deletePlaylist(item.uid)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(ignored -> deleteSuccessful.show()));
break;
default:
break;
}
};
final String videoCount = getResources().getQuantityString(R.plurals.videos,
(int) item.getStreamCount(), (int) item.getStreamCount());
new InfoItemDialog(getActivity(), commands, actions, item.getName(), videoCount).show();
})
.setNegativeButton(R.string.cancel, null)
.show();
}
private void resetFragment() {
if (disposables != null) disposables.clear();
if (infoListAdapter != null) infoListAdapter.clearStreamItemList();
if (itemListAdapter != null) itemListAdapter.clearStreamItemList();
}
///////////////////////////////////////////////////////////////////////////
@ -254,12 +234,12 @@ public class BookmarkFragment extends BaseStateFragment<List<PlaylistMetadataEnt
public void handleResult(@NonNull List<PlaylistMetadataEntry> result) {
super.handleResult(result);
infoListAdapter.clearStreamItemList();
itemListAdapter.clearStreamItemList();
if (result.isEmpty()) {
showEmptyState();
} else {
infoListAdapter.addInfoItemList(infoItemsOf(result));
itemListAdapter.addInfoItemList(infoItemsOf(result));
if (itemsListState != null) {
itemsList.getLayoutManager().onRestoreInstanceState(itemsListState);
itemsListState = null;
@ -269,13 +249,9 @@ public class BookmarkFragment extends BaseStateFragment<List<PlaylistMetadataEnt
}
private List<InfoItem> infoItemsOf(List<PlaylistMetadataEntry> playlists) {
List<InfoItem> playlistInfoItems = new ArrayList<>(playlists.size());
for (final PlaylistMetadataEntry playlist : playlists) {
playlistInfoItems.add(playlist.toStoredPlaylistInfoItem());
}
Collections.sort(playlistInfoItems, (o1, o2) -> o1.name.compareToIgnoreCase(o2.name));
return playlistInfoItems;
private List<PlaylistMetadataEntry> infoItemsOf(List<PlaylistMetadataEntry> playlists) {
Collections.sort(playlists, (o1, o2) -> o1.name.compareToIgnoreCase(o2.name));
return playlists;
}
/*//////////////////////////////////////////////////////////////////////////

View File

@ -0,0 +1,22 @@
package org.schabi.newpipe.fragments.local.bookmark;
import org.schabi.newpipe.R;
import org.schabi.newpipe.database.stream.StreamStatisticsEntry;
import java.util.Collections;
import java.util.List;
public class MostPlayedFragment extends StatisticsPlaylistFragment {
@Override
protected String getName() {
return getString(R.string.title_most_played);
}
@Override
protected List<StreamStatisticsEntry> processResult(List<StreamStatisticsEntry> results) {
Collections.sort(results, (left, right) ->
((Long) right.watchCount).compareTo(left.watchCount));
return results;
}
}

View File

@ -1,4 +1,4 @@
package org.schabi.newpipe.fragments.local;
package org.schabi.newpipe.fragments.local.bookmark;
import android.app.Activity;
import android.content.Context;
@ -14,14 +14,13 @@ import android.view.ViewGroup;
import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription;
import org.schabi.newpipe.R;
import org.schabi.newpipe.database.LocalItem;
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.fragments.local.BaseLocalListFragment;
import org.schabi.newpipe.fragments.local.OnCustomItemGesture;
import org.schabi.newpipe.history.HistoryRecordManager;
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;
import org.schabi.newpipe.report.UserAction;
@ -36,7 +35,7 @@ import io.reactivex.android.schedulers.AndroidSchedulers;
import static org.schabi.newpipe.util.AnimationUtils.animateView;
public abstract class StatisticsPlaylistFragment
extends BaseListFragment<List<StreamStatisticsEntry>, Void> {
extends BaseLocalListFragment<List<StreamStatisticsEntry>, Void> {
private View headerRootLayout;
private View playlistControl;
@ -57,9 +56,7 @@ public abstract class StatisticsPlaylistFragment
protected abstract String getName();
protected abstract List<InfoItem> processResult(final List<StreamStatisticsEntry> results);
protected abstract String getAdditionalDetail(final StreamStatisticsInfoItem infoItem);
protected abstract List<StreamStatisticsEntry> processResult(final List<StreamStatisticsEntry> results);
///////////////////////////////////////////////////////////////////////////
// Fragment LifeCycle
@ -106,8 +103,6 @@ public abstract class StatisticsPlaylistFragment
@Override
protected void initViews(View rootView, Bundle savedInstanceState) {
super.initViews(rootView, savedInstanceState);
infoListAdapter.useMiniItemVariants(true);
setFragmentTitle(getName());
}
@ -127,27 +122,31 @@ public abstract class StatisticsPlaylistFragment
protected void initListeners() {
super.initListeners();
infoListAdapter.setOnStreamSelectedListener(new OnInfoItemGesture<StreamInfoItem>() {
itemListAdapter.setSelectedListener(new OnCustomItemGesture<LocalItem>() {
@Override
public void selected(StreamInfoItem selectedItem) {
NavigationHelper.openVideoDetailFragment(getFragmentManager(),
selectedItem.getServiceId(), selectedItem.url, selectedItem.getName());
public void selected(LocalItem selectedItem) {
if (selectedItem instanceof StreamStatisticsEntry) {
final StreamStatisticsEntry item = (StreamStatisticsEntry) selectedItem;
NavigationHelper.openVideoDetailFragment(getFragmentManager(),
item.serviceId, item.url, item.title);
}
}
@Override
public void held(StreamInfoItem selectedItem) {
showStreamDialog(selectedItem);
public void held(LocalItem selectedItem) {
if (selectedItem instanceof StreamStatisticsEntry) {
showStreamDialog((StreamStatisticsEntry) selectedItem);
}
}
});
}
@Override
protected void showStreamDialog(final StreamInfoItem item) {
private void showStreamDialog(final StreamStatisticsEntry item) {
final Context context = getContext();
final Activity activity = getActivity();
if (context == null || context.getResources() == null
|| getActivity() == null || !(item instanceof StreamStatisticsInfoItem)) return;
if (context == null || context.getResources() == null || getActivity() == null) return;
final StreamInfoItem infoItem = item.toStreamInfoItem();
final String[] commands = new String[]{
context.getResources().getString(R.string.enqueue_on_background),
@ -158,13 +157,13 @@ public abstract class StatisticsPlaylistFragment
};
final DialogInterface.OnClickListener actions = (dialogInterface, i) -> {
final int index = Math.max(infoListAdapter.getItemsList().indexOf(item), 0);
final int index = Math.max(itemListAdapter.getItemsList().indexOf(item), 0);
switch (i) {
case 0:
NavigationHelper.enqueueOnBackgroundPlayer(context, new SinglePlayQueue(item));
NavigationHelper.enqueueOnBackgroundPlayer(context, new SinglePlayQueue(infoItem));
break;
case 1:
NavigationHelper.enqueueOnPopupPlayer(activity, new SinglePlayQueue(item));
NavigationHelper.enqueueOnPopupPlayer(activity, new SinglePlayQueue(infoItem));
break;
case 2:
NavigationHelper.playOnMainPlayer(context, getPlayQueue(index));
@ -180,13 +179,12 @@ public abstract class StatisticsPlaylistFragment
}
};
final String detail = getAdditionalDetail((StreamStatisticsInfoItem) item);
new InfoItemDialog(getActivity(), commands, actions, item.getName(), detail).show();
new InfoItemDialog(getActivity(), infoItem, commands, actions).show();
}
private void resetFragment() {
if (databaseSubscription != null) databaseSubscription.cancel();
if (infoListAdapter != null) infoListAdapter.clearStreamItemList();
if (itemListAdapter != null) itemListAdapter.clearStreamItemList();
}
///////////////////////////////////////////////////////////////////////////
@ -241,7 +239,7 @@ public abstract class StatisticsPlaylistFragment
@Override
public void handleResult(@NonNull List<StreamStatisticsEntry> result) {
super.handleResult(result);
infoListAdapter.clearStreamItemList();
itemListAdapter.clearStreamItemList();
if (result.isEmpty()) {
showEmptyState();
@ -251,7 +249,7 @@ public abstract class StatisticsPlaylistFragment
animateView(headerRootLayout, true, 100);
animateView(itemsList, true, 300);
infoListAdapter.addInfoItemList(processResult(result));
itemListAdapter.addInfoItemList(processResult(result));
if (itemsListState != null) {
itemsList.getLayoutManager().onRestoreInstanceState(itemsListState);
itemsListState = null;
@ -267,20 +265,6 @@ public abstract class StatisticsPlaylistFragment
hideLoading();
}
/*//////////////////////////////////////////////////////////////////////////
// Contract
//////////////////////////////////////////////////////////////////////////*/
@Override
protected void loadMoreItems() {
// Do nothing
}
@Override
protected boolean hasMoreItems() {
return false;
}
///////////////////////////////////////////////////////////////////////////
// Fragment Error Handling
///////////////////////////////////////////////////////////////////////////
@ -310,10 +294,12 @@ public abstract class StatisticsPlaylistFragment
}
private PlayQueue getPlayQueue(final int index) {
final List<InfoItem> infoItems = infoListAdapter.getItemsList();
final List<LocalItem> infoItems = itemListAdapter.getItemsList();
List<StreamInfoItem> streamInfoItems = new ArrayList<>(infoItems.size());
for (final InfoItem item : infoItems) {
if (item instanceof StreamInfoItem) streamInfoItems.add((StreamInfoItem) item);
for (final LocalItem item : infoItems) {
if (item instanceof StreamStatisticsEntry) {
streamInfoItems.add(((StreamStatisticsEntry) item).toStreamInfoItem());
}
}
return new SinglePlayQueue(streamInfoItems, index);
}

View File

@ -0,0 +1,21 @@
package org.schabi.newpipe.fragments.local.bookmark;
import org.schabi.newpipe.R;
import org.schabi.newpipe.database.stream.StreamStatisticsEntry;
import java.util.Collections;
import java.util.List;
public class WatchHistoryFragment extends StatisticsPlaylistFragment {
@Override
protected String getName() {
return getString(R.string.title_watch_history);
}
@Override
protected List<StreamStatisticsEntry> processResult(List<StreamStatisticsEntry> results) {
Collections.sort(results, (left, right) ->
right.latestAccessDate.compareTo(left.latestAccessDate));
return results;
}
}

View File

@ -0,0 +1,56 @@
package org.schabi.newpipe.fragments.local.holder;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import com.nostra13.universalimageloader.core.DisplayImageOptions;
import org.schabi.newpipe.database.LocalItem;
import org.schabi.newpipe.fragments.local.LocalItemBuilder;
import java.text.DateFormat;
/*
* Created by Christian Schabesberger on 12.02.17.
*
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
* InfoItemHolder.java is part of NewPipe.
*
* NewPipe is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* NewPipe is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
public abstract class LocalItemHolder extends RecyclerView.ViewHolder {
protected final LocalItemBuilder itemBuilder;
public LocalItemHolder(LocalItemBuilder itemBuilder, int layoutId, ViewGroup parent) {
super(LayoutInflater.from(itemBuilder.getContext())
.inflate(layoutId, parent, false));
this.itemBuilder = itemBuilder;
}
public abstract void updateFromItem(final LocalItem item, final DateFormat dateFormat);
/*//////////////////////////////////////////////////////////////////////////
// ImageLoaderOptions
//////////////////////////////////////////////////////////////////////////*/
/**
* Base display options
*/
public static final DisplayImageOptions BASE_DISPLAY_IMAGE_OPTIONS =
new DisplayImageOptions.Builder()
.cacheInMemory(true)
.build();
}

View File

@ -0,0 +1,74 @@
package org.schabi.newpipe.fragments.local.holder;
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.database.LocalItem;
import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry;
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 LocalPlaylistItemHolder(LocalItemBuilder infoItemBuilder, ViewGroup parent) {
this(infoItemBuilder, R.layout.list_playlist_mini_item, parent);
}
@Override
public void updateFromItem(final LocalItem localItem, final DateFormat dateFormat) {
if (!(localItem instanceof PlaylistMetadataEntry)) return;
final PlaylistMetadataEntry item = (PlaylistMetadataEntry) localItem;
itemTitleView.setText(item.name);
itemStreamCountView.setText(String.valueOf(item.streamCount));
itemUploaderView.setVisibility(View.INVISIBLE);
itemBuilder.getImageLoader().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;
});
}
/**
* 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

@ -1,7 +1,6 @@
package org.schabi.newpipe.info_list.holder;
package org.schabi.newpipe.fragments.local.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;
@ -11,40 +10,44 @@ 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.database.LocalItem;
import org.schabi.newpipe.database.playlist.PlaylistStreamEntry;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.fragments.local.LocalItemBuilder;
import org.schabi.newpipe.util.Localization;
public class StreamPlaylistInfoItemHolder extends InfoItemHolder {
import java.text.DateFormat;
public class LocalPlaylistStreamItemHolder extends LocalItemHolder {
public final ImageView itemThumbnailView;
public final TextView itemVideoTitleView;
public final TextView itemUploaderView;
public final TextView itemAdditionalDetailsView;
public final TextView itemDurationView;
public final View itemHandleView;
StreamPlaylistInfoItemHolder(InfoItemBuilder infoItemBuilder, int layoutId, ViewGroup parent) {
LocalPlaylistStreamItemHolder(LocalItemBuilder 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);
itemAdditionalDetailsView = itemView.findViewById(R.id.itemAdditionalDetails);
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);
public LocalPlaylistStreamItemHolder(LocalItemBuilder infoItemBuilder, ViewGroup parent) {
this(infoItemBuilder, R.layout.list_stream_playlist_item, parent);
}
@Override
public void updateFromItem(final InfoItem infoItem) {
if (!(infoItem instanceof StreamInfoItem)) return;
final StreamInfoItem item = (StreamInfoItem) infoItem;
public void updateFromItem(final LocalItem localItem, final DateFormat dateFormat) {
if (!(localItem instanceof PlaylistStreamEntry)) return;
final PlaylistStreamEntry item = (PlaylistStreamEntry) localItem;
itemVideoTitleView.setText(item.getName());
itemUploaderView.setText(item.uploader_name);
itemVideoTitleView.setText(item.title);
itemAdditionalDetailsView.setText(Localization.concatenateStrings(item.uploader,
NewPipe.getNameOfService(item.serviceId)));
if (item.duration > 0) {
itemDurationView.setText(Localization.getDurationString(item.duration));
@ -56,19 +59,19 @@ public class StreamPlaylistInfoItemHolder extends InfoItemHolder {
}
// 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);
itemBuilder.getImageLoader().displayImage(item.thumbnailUrl, itemThumbnailView,
LocalPlaylistStreamItemHolder.DISPLAY_THUMBNAIL_OPTIONS);
itemView.setOnClickListener(view -> {
if (itemBuilder.getOnStreamSelectedListener() != null) {
itemBuilder.getOnStreamSelectedListener().selected(item);
if (itemBuilder.getOnItemSelectedListener() != null) {
itemBuilder.getOnItemSelectedListener().selected(item);
}
});
itemView.setLongClickable(true);
itemView.setOnLongClickListener(view -> {
if (itemBuilder.getOnStreamSelectedListener() != null) {
itemBuilder.getOnStreamSelectedListener().held(item);
if (itemBuilder.getOnItemSelectedListener() != null) {
itemBuilder.getOnItemSelectedListener().held(item);
}
return true;
});
@ -77,13 +80,13 @@ public class StreamPlaylistInfoItemHolder extends InfoItemHolder {
itemHandleView.setOnTouchListener(getOnTouchListener(item));
}
private View.OnTouchListener getOnTouchListener(final StreamInfoItem item) {
private View.OnTouchListener getOnTouchListener(final PlaylistStreamEntry item) {
return (view, motionEvent) -> {
view.performClick();
if (itemBuilder != null &&
motionEvent.getActionMasked() == MotionEvent.ACTION_DOWN) {
itemBuilder.getOnStreamSelectedListener()
.drag(item, StreamPlaylistInfoItemHolder.this);
itemBuilder.getOnItemSelectedListener().drag(item,
LocalPlaylistStreamItemHolder.this);
}
return false;
};

View File

@ -0,0 +1,119 @@
package org.schabi.newpipe.fragments.local.holder;
import android.support.v4.content.ContextCompat;
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.database.LocalItem;
import org.schabi.newpipe.database.stream.StreamStatisticsEntry;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.fragments.local.LocalItemBuilder;
import org.schabi.newpipe.util.Localization;
import java.text.DateFormat;
/*
* Created by Christian Schabesberger on 01.08.16.
* <p>
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
* StreamInfoItemHolder.java is part of NewPipe.
* <p>
* NewPipe is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* <p>
* NewPipe is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
* <p>
* You should have received a copy of the GNU General Public License
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
public class LocalStatisticStreamItemHolder extends LocalItemHolder {
public final ImageView itemThumbnailView;
public final TextView itemVideoTitleView;
public final TextView itemUploaderView;
public final TextView itemDurationView;
public final TextView itemAdditionalDetails;
LocalStatisticStreamItemHolder(LocalItemBuilder 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);
itemAdditionalDetails = itemView.findViewById(R.id.itemAdditionalDetails);
}
public LocalStatisticStreamItemHolder(LocalItemBuilder infoItemBuilder, ViewGroup parent) {
this(infoItemBuilder, R.layout.list_stream_item, parent);
}
private String getStreamInfoDetailLine(final StreamStatisticsEntry entry,
final DateFormat dateFormat) {
final String watchCount = Localization.shortViewCount(itemBuilder.getContext(),
entry.watchCount);
final String uploadDate = dateFormat.format(entry.latestAccessDate);
final String serviceName = NewPipe.getNameOfService(entry.serviceId);
return Localization.concatenateStrings(watchCount, uploadDate, serviceName);
}
@Override
public void updateFromItem(final LocalItem localItem, final DateFormat dateFormat) {
if (!(localItem instanceof StreamStatisticsEntry)) return;
final StreamStatisticsEntry item = (StreamStatisticsEntry) localItem;
itemVideoTitleView.setText(item.title);
itemUploaderView.setText(item.uploader);
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);
}
itemAdditionalDetails.setText(getStreamInfoDetailLine(item, dateFormat));
// Default thumbnail is shown on error, while loading and if the url is empty
itemBuilder.getImageLoader().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;
});
}
/**
* Display options for stream thumbnails
*/
public 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

@ -32,6 +32,7 @@ import io.reactivex.Flowable;
import io.reactivex.Single;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.disposables.Disposable;
import static org.schabi.newpipe.util.AnimationUtils.animateView;
@ -169,8 +170,14 @@ public abstract class HistoryFragment<E> extends BaseFragment
private void clearHistory() {
final Collection<E> itemsToDelete = new ArrayList<>(mHistoryAdapter.getItems());
disposables.add(delete(itemsToDelete).observeOn(AndroidSchedulers.mainThread())
.subscribe());
final Disposable deletion = delete(itemsToDelete)
.observeOn(AndroidSchedulers.mainThread())
.subscribe();
final Disposable cleanUp = historyRecordManager.removeOrphanedRecords()
.observeOn(AndroidSchedulers.mainThread())
.subscribe();
disposables.addAll(deletion, cleanUp);
makeSnackbar(R.string.history_cleared);
mHistoryAdapter.clear();

View File

@ -48,6 +48,14 @@ public class HistoryRecordManager {
streamHistoryKey = context.getString(R.string.enable_watch_history_key);
}
public Single<Integer> removeOrphanedRecords() {
return Single.fromCallable(streamTable::deleteOrphans).subscribeOn(Schedulers.io());
}
///////////////////////////////////////////////////////
// Watch History
///////////////////////////////////////////////////////
public Maybe<Long> onViewed(final StreamInfo info) {
if (!isStreamHistoryEnabled()) return Maybe.empty();

View File

@ -55,10 +55,6 @@ 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;

View File

@ -1,7 +1,5 @@
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> {
@ -11,8 +9,4 @@ public abstract class OnInfoItemGesture<T extends InfoItem> {
public void held(T selectedItem) {
// Optional gesture
}
public void drag(T selectedItem, RecyclerView.ViewHolder viewHolder) {
// Optional gesture
}
}

View File

@ -1,18 +0,0 @@
package org.schabi.newpipe.info_list.stored;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.extractor.stream.StreamType;
public class StreamEntityInfoItem extends StreamInfoItem {
protected final long streamId;
public StreamEntityInfoItem(final long streamId, final int serviceId,
final String url, final String name, final StreamType type) {
super(serviceId, url, name, type);
this.streamId = streamId;
}
public long getStreamId() {
return streamId;
}
}

View File

@ -1,31 +0,0 @@
package org.schabi.newpipe.info_list.stored;
import org.schabi.newpipe.extractor.stream.StreamType;
import java.util.Date;
public final class StreamStatisticsInfoItem extends StreamEntityInfoItem {
private Date latestAccessDate;
private long watchCount;
public StreamStatisticsInfoItem(final long streamId, final int serviceId,
final String url, final String name, final StreamType type) {
super(streamId, serviceId, url, name, type);
}
public Date getLatestAccessDate() {
return latestAccessDate;
}
public void setLatestAccessDate(Date latestAccessDate) {
this.latestAccessDate = latestAccessDate;
}
public long getWatchCount() {
return watchCount;
}
public void setWatchCount(long watchCount) {
this.watchCount = watchCount;
}
}

View File

@ -4,6 +4,7 @@ import android.content.Context;
import android.content.SharedPreferences;
import android.content.res.Resources;
import android.preference.PreferenceManager;
import android.support.annotation.NonNull;
import android.support.annotation.PluralsRes;
import android.support.annotation.StringRes;
import android.text.TextUtils;
@ -14,7 +15,9 @@ import java.text.DateFormat;
import java.text.NumberFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.Locale;
/*
@ -39,9 +42,33 @@ import java.util.Locale;
public class Localization {
public final static String DOT_SEPARATOR = "";
private Localization() {
}
@NonNull
public static String concatenateStrings(final String... strings) {
return concatenateStrings(Arrays.asList(strings));
}
@NonNull
public static String concatenateStrings(final List<String> strings) {
if (strings.isEmpty()) return "";
final StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append(strings.get(0));
for (int i = 1; i < strings.size(); i++) {
final String string = strings.get(i);
if (!TextUtils.isEmpty(string)) {
stringBuilder.append(DOT_SEPARATOR).append(strings.get(i));
}
}
return stringBuilder.toString();
}
public static Locale getPreferredLocale(Context context) {
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context);

View File

@ -35,8 +35,8 @@ import org.schabi.newpipe.fragments.list.kiosk.KioskFragment;
import org.schabi.newpipe.fragments.list.playlist.PlaylistFragment;
import org.schabi.newpipe.fragments.list.search.SearchFragment;
import org.schabi.newpipe.fragments.local.LocalPlaylistFragment;
import org.schabi.newpipe.fragments.local.MostPlayedFragment;
import org.schabi.newpipe.fragments.local.WatchHistoryFragment;
import org.schabi.newpipe.fragments.local.bookmark.MostPlayedFragment;
import org.schabi.newpipe.fragments.local.bookmark.WatchHistoryFragment;
import org.schabi.newpipe.history.HistoryActivity;
import org.schabi.newpipe.player.BackgroundPlayer;
import org.schabi.newpipe.player.BackgroundPlayerActivity;

View File

@ -70,7 +70,7 @@
tools:text="Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc tristique..."/>
<TextView
android:id="@+id/itemUploaderView"
android:id="@+id/itemAdditionalDetails"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/itemVideoTitleView"

View File

@ -14,6 +14,11 @@
android:layout_marginLeft="8dp"
android:layout_marginRight="8dp"
android:layout_marginTop="6dp"
android:layout_toLeftOf="@id/playlist_stream_count"
android:layout_toStartOf="@id/playlist_stream_count"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:ellipsize="marquee"
android:fadingEdge="horizontal"
android:marqueeRepeatLimit="marquee_forever"
@ -21,13 +26,13 @@
android:singleLine="true"
android:textAppearance="?android:attr/textAppearanceLarge"
android:textSize="@dimen/playlist_detail_title_text_size"
tools:text="Mix musics #23 title Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc tristique vitae sem vitae blanditLorem ipsumLorem ipsumLorem ipsumLorem ipsumLorem ipsumLorem ipsumLorem ipsum" />
tools:text="Mix musics #23 title Lorem ipsum dolor sit amet, consectetur..." />
<TextView
android:id="@+id/playlist_stream_count"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/playlist_title_view"
android:layout_alignBottom="@id/playlist_title_view"
android:layout_alignParentRight="true"
android:layout_marginRight="6dp"
android:ellipsize="end"

View File

@ -3,10 +3,12 @@
xmlns:tools="http://schemas.android.com/tools"
tools:context=".player.BackgroundPlayerActivity">
<item android:id="@+id/action_append_playlist"
android:orderInCategory="981"
<item
android:id="@+id/action_append_playlist"
android:icon="?attr/playlist_add"
android:title="@string/append_playlist"
app:showAsAction="never"/>
android:visible="true"
app:showAsAction="ifRoom"/>
<item android:id="@+id/action_settings"
android:orderInCategory="990"

View File

@ -232,6 +232,7 @@
<string name="delete_one">Delete One</string>
<string name="delete_all">Delete All</string>
<string name="checksum">Checksum</string>
<string name="dismiss">Dismiss</string>
<!-- Fragment -->
<string name="add">New mission</string>
@ -380,6 +381,8 @@
<!-- Local Playlist -->
<string name="create_playlist">Create New Playlist</string>
<string name="delete_playlist">Delete Playlist</string>
<string name="rename_playlist">Rename Playlist</string>
<string name="playlist_name_input">Name</string>
<string name="append_playlist">Add To Playlist</string>
<string name="delete_playlist_prompt">Do you want to delete this playlist?</string>
</resources>