-Added basic UI for local playlists.

-Added UI for watch history and most played fragments.
-Added stream state table for storing playback timestamp and future usage.
-Enabled playlist deletion.
This commit is contained in:
John Zhen Mo 2018-01-16 21:12:03 -08:00
parent 38946e4b0f
commit ba9d0d7707
28 changed files with 1446 additions and 58 deletions

View file

@ -14,8 +14,10 @@ import org.schabi.newpipe.database.stream.dao.StreamHistoryDAO;
import org.schabi.newpipe.database.stream.dao.StreamDAO; import org.schabi.newpipe.database.stream.dao.StreamDAO;
import org.schabi.newpipe.database.playlist.model.PlaylistEntity; import org.schabi.newpipe.database.playlist.model.PlaylistEntity;
import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity; import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity;
import org.schabi.newpipe.database.stream.dao.StreamStateDAO;
import org.schabi.newpipe.database.stream.model.StreamHistoryEntity; import org.schabi.newpipe.database.stream.model.StreamHistoryEntity;
import org.schabi.newpipe.database.stream.model.StreamEntity; import org.schabi.newpipe.database.stream.model.StreamEntity;
import org.schabi.newpipe.database.stream.model.StreamStateEntity;
import org.schabi.newpipe.database.subscription.SubscriptionDAO; import org.schabi.newpipe.database.subscription.SubscriptionDAO;
import org.schabi.newpipe.database.subscription.SubscriptionEntity; import org.schabi.newpipe.database.subscription.SubscriptionEntity;
@ -23,8 +25,8 @@ import org.schabi.newpipe.database.subscription.SubscriptionEntity;
@Database( @Database(
entities = { entities = {
SubscriptionEntity.class, WatchHistoryEntry.class, SearchHistoryEntry.class, SubscriptionEntity.class, WatchHistoryEntry.class, SearchHistoryEntry.class,
StreamEntity.class, StreamHistoryEntity.class, PlaylistEntity.class, StreamEntity.class, StreamHistoryEntity.class, StreamStateEntity.class,
PlaylistStreamEntity.class PlaylistEntity.class, PlaylistStreamEntity.class
}, },
version = 1, version = 1,
exportSchema = false exportSchema = false
@ -43,6 +45,8 @@ public abstract class AppDatabase extends RoomDatabase {
public abstract StreamHistoryDAO streamHistoryDAO(); public abstract StreamHistoryDAO streamHistoryDAO();
public abstract StreamStateDAO streamStateDAO();
public abstract PlaylistDAO playlistDAO(); public abstract PlaylistDAO playlistDAO();
public abstract PlaylistStreamDAO playlistStreamDAO(); public abstract PlaylistStreamDAO playlistStreamDAO();

View file

@ -32,4 +32,7 @@ public abstract class PlaylistDAO implements BasicDAO<PlaylistEntity> {
@Query("SELECT * FROM " + PLAYLIST_TABLE + " WHERE " + PLAYLIST_ID + " = :playlistId") @Query("SELECT * FROM " + PLAYLIST_TABLE + " WHERE " + PLAYLIST_ID + " = :playlistId")
public abstract Flowable<List<PlaylistEntity>> getPlaylist(final long playlistId); public abstract Flowable<List<PlaylistEntity>> getPlaylist(final long playlistId);
@Query("DELETE FROM " + PLAYLIST_TABLE + " WHERE " + PLAYLIST_ID + " = :playlistId")
public abstract int deletePlaylist(final long playlistId);
} }

View file

@ -4,7 +4,9 @@ import android.arch.persistence.room.ColumnInfo;
import org.schabi.newpipe.database.stream.model.StreamHistoryEntity; import org.schabi.newpipe.database.stream.model.StreamHistoryEntity;
import org.schabi.newpipe.database.stream.model.StreamEntity; 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.extractor.stream.StreamType;
import org.schabi.newpipe.info_list.stored.StreamStatisticsInfoItem;
import java.util.Date; import java.util.Date;
@ -51,4 +53,15 @@ public class StreamStatisticsEntry {
this.latestAccessDate = latestAccessDate; this.latestAccessDate = latestAccessDate;
this.watchCount = watchCount; this.watchCount = watchCount;
} }
public StreamStatisticsInfoItem toStreamStatisticsInfoItem() {
StreamStatisticsInfoItem item =
new StreamStatisticsInfoItem(uid, serviceId, url, title, streamType);
item.setDuration(duration);
item.setUploaderName(uploader);
item.setThumbnailUrl(thumbnailUrl);
item.setLatestAccessDate(latestAccessDate);
item.setWatchCount(watchCount);
return item;
}
} }

View file

@ -0,0 +1,33 @@
package org.schabi.newpipe.database.stream.dao;
import android.arch.persistence.room.Dao;
import android.arch.persistence.room.Query;
import org.schabi.newpipe.database.BasicDAO;
import org.schabi.newpipe.database.stream.model.StreamStateEntity;
import java.util.List;
import io.reactivex.Flowable;
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.JOIN_STREAM_ID;
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_STATE_TABLE;
@Dao
public abstract class StreamStateDAO implements BasicDAO<StreamStateEntity> {
@Override
@Query("SELECT * FROM " + STREAM_STATE_TABLE)
public abstract Flowable<List<StreamStateEntity>> getAll();
@Override
@Query("DELETE FROM " + STREAM_STATE_TABLE)
public abstract int deleteAll();
@Override
public Flowable<List<StreamStateEntity>> listByService(int serviceId) {
throw new UnsupportedOperationException();
}
@Query("DELETE FROM " + STREAM_STATE_TABLE + " WHERE " + JOIN_STREAM_ID + " = :streamId")
public abstract int deleteState(final long streamId);
}

View file

@ -0,0 +1,51 @@
package org.schabi.newpipe.database.stream.model;
import android.arch.persistence.room.ColumnInfo;
import android.arch.persistence.room.Entity;
import android.arch.persistence.room.ForeignKey;
import static android.arch.persistence.room.ForeignKey.CASCADE;
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.JOIN_STREAM_ID;
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_STATE_TABLE;
@Entity(tableName = STREAM_STATE_TABLE,
primaryKeys = {JOIN_STREAM_ID},
foreignKeys = {
@ForeignKey(entity = StreamEntity.class,
parentColumns = StreamEntity.STREAM_ID,
childColumns = JOIN_STREAM_ID,
onDelete = CASCADE, onUpdate = CASCADE)
})
public class StreamStateEntity {
final public static String STREAM_STATE_TABLE = "stream_state";
final public static String JOIN_STREAM_ID = "stream_id";
final public static String STREAM_PROGRESS_TIME = "progress_time";
@ColumnInfo(name = JOIN_STREAM_ID)
private long streamUid;
@ColumnInfo(name = STREAM_PROGRESS_TIME)
private long progressTime;
public StreamStateEntity(long streamUid, long progressTime) {
this.streamUid = streamUid;
this.progressTime = progressTime;
}
public long getStreamUid() {
return streamUid;
}
public void setStreamUid(long streamUid) {
this.streamUid = streamUid;
}
public long getProgressTime() {
return progressTime;
}
public void setProgressTime(long progressTime) {
this.progressTime = progressTime;
}
}

View file

@ -29,6 +29,7 @@ import org.schabi.newpipe.extractor.kiosk.KioskList;
import org.schabi.newpipe.fragments.list.channel.ChannelFragment; import org.schabi.newpipe.fragments.list.channel.ChannelFragment;
import org.schabi.newpipe.fragments.list.feed.FeedFragment; import org.schabi.newpipe.fragments.list.feed.FeedFragment;
import org.schabi.newpipe.fragments.list.kiosk.KioskFragment; import org.schabi.newpipe.fragments.list.kiosk.KioskFragment;
import org.schabi.newpipe.fragments.local.BookmarkFragment;
import org.schabi.newpipe.fragments.subscription.SubscriptionFragment; import org.schabi.newpipe.fragments.subscription.SubscriptionFragment;
import org.schabi.newpipe.report.ErrorActivity; import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.report.UserAction; import org.schabi.newpipe.report.UserAction;
@ -87,9 +88,11 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
if (isSubscriptionsPageOnlySelected()) { if (isSubscriptionsPageOnlySelected()) {
tabLayout.getTabAt(0).setIcon(channelIcon); tabLayout.getTabAt(0).setIcon(channelIcon);
tabLayout.getTabAt(1).setText(R.string.tab_bookmarks);
} else { } else {
tabLayout.getTabAt(0).setIcon(whatsHotIcon); tabLayout.getTabAt(0).setIcon(whatsHotIcon);
tabLayout.getTabAt(1).setIcon(channelIcon); tabLayout.getTabAt(1).setIcon(channelIcon);
tabLayout.getTabAt(2).setText(R.string.tab_bookmarks);
} }
} }
@ -147,7 +150,6 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
} }
private class PagerAdapter extends FragmentPagerAdapter { private class PagerAdapter extends FragmentPagerAdapter {
PagerAdapter(FragmentManager fm) { PagerAdapter(FragmentManager fm) {
super(fm); super(fm);
} }
@ -158,7 +160,15 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
case 0: case 0:
return isSubscriptionsPageOnlySelected() ? new SubscriptionFragment() : getMainPageFragment(); return isSubscriptionsPageOnlySelected() ? new SubscriptionFragment() : getMainPageFragment();
case 1: case 1:
return new SubscriptionFragment(); if(PreferenceManager.getDefaultSharedPreferences(getActivity())
.getString(getString(R.string.main_page_content_key), getString(R.string.blank_page_key))
.equals(getString(R.string.subscription_page_key))) {
return new BookmarkFragment();
} else {
return new SubscriptionFragment();
}
case 2:
return new BookmarkFragment();
default: default:
return new BlankFragment(); return new BlankFragment();
} }
@ -172,7 +182,7 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
@Override @Override
public int getCount() { public int getCount() {
return isSubscriptionsPageOnlySelected() ? 1 : 2; return isSubscriptionsPageOnlySelected() ? 2 : 3;
} }
} }
@ -187,6 +197,8 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
} }
private Fragment getMainPageFragment() { private Fragment getMainPageFragment() {
if (getActivity() == null) return new BlankFragment();
try { try {
SharedPreferences preferences = SharedPreferences preferences =
PreferenceManager.getDefaultSharedPreferences(getActivity()); PreferenceManager.getDefaultSharedPreferences(getActivity());
@ -216,6 +228,10 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
ChannelFragment fragment = ChannelFragment.getInstance(serviceId, url, name); ChannelFragment fragment = ChannelFragment.getInstance(serviceId, url, name);
fragment.useAsFrontPage(true); fragment.useAsFrontPage(true);
return fragment; return fragment;
} else if (setMainPage.equals(getString(R.string.bookmark_page_key))) {
final BookmarkFragment fragment = new BookmarkFragment();
fragment.useAsFrontPage(true);
return fragment;
} else { } else {
return new BlankFragment(); return new BlankFragment();
} }

View file

@ -0,0 +1,318 @@
package org.schabi.newpipe.fragments.local;
import android.content.Context;
import android.content.DialogInterface;
import android.os.Bundle;
import android.os.Parcelable;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
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.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.stored.LocalPlaylistInfoItem;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.NavigationHelper;
import java.util.ArrayList;
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;
public class BookmarkFragment extends BaseStateFragment<List<PlaylistMetadataEntry>> {
private View watchHistoryButton;
private View mostWatchedButton;
private InfoListAdapter infoListAdapter;
private RecyclerView itemsList;
@State
protected Parcelable itemsListState;
private Subscription databaseSubscription;
private CompositeDisposable disposables = new CompositeDisposable();
private LocalPlaylistManager localPlaylistManager;
///////////////////////////////////////////////////////////////////////////
// Fragment LifeCycle
///////////////////////////////////////////////////////////////////////////
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
if(isVisibleToUser && activity != null && activity.getSupportActionBar() != null) {
activity.getSupportActionBar().setTitle(R.string.tab_bookmarks);
}
}
@Override
public void onAttach(Context context) {
super.onAttach(context);
infoListAdapter = new InfoListAdapter(activity);
localPlaylistManager = new LocalPlaylistManager(NewPipeDatabase.getInstance(context));
}
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater,
@Nullable ViewGroup container,
Bundle savedInstanceState) {
if (activity.getSupportActionBar() != null) {
activity.getSupportActionBar().setDisplayShowTitleEnabled(true);
}
activity.setTitle(R.string.tab_bookmarks);
if(useAsFrontPage) {
activity.getSupportActionBar().setDisplayHomeAsUpEnabled(false);
}
return inflater.inflate(R.layout.fragment_bookmarks, container, false);
}
@Override
public void onPause() {
super.onPause();
itemsListState = itemsList.getLayoutManager().onSaveInstanceState();
}
@Override
public void onDestroyView() {
if (disposables != null) disposables.clear();
if (databaseSubscription != null) databaseSubscription.cancel();
super.onDestroyView();
}
@Override
public void onDestroy() {
if (disposables != null) disposables.dispose();
if (databaseSubscription != null) databaseSubscription.cancel();
disposables = null;
databaseSubscription = null;
localPlaylistManager = null;
super.onDestroy();
}
///////////////////////////////////////////////////////////////////////////
// Fragment Views
///////////////////////////////////////////////////////////////////////////
@Override
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));
final View headerRootLayout = activity.getLayoutInflater()
.inflate(R.layout.bookmark_header, itemsList, false);
watchHistoryButton = headerRootLayout.findViewById(R.id.watchHistory);
mostWatchedButton = headerRootLayout.findViewById(R.id.mostWatched);
infoListAdapter.setHeader(headerRootLayout);
infoListAdapter.useMiniItemVariants(true);
itemsList.setAdapter(infoListAdapter);
}
@Override
protected void initListeners() {
super.initListeners();
infoListAdapter.setOnPlaylistSelectedListener(new InfoItemBuilder.OnInfoItemSelectedListener<PlaylistInfoItem>() {
@Override
public void selected(PlaylistInfoItem selectedItem) {
// Requires the parent fragment to find holder for fragment replacement
if (selectedItem instanceof LocalPlaylistInfoItem && getParentFragment() != null) {
final long playlistId = ((LocalPlaylistInfoItem) selectedItem).getPlaylistId();
NavigationHelper.openLocalPlaylistFragment(
getParentFragment().getFragmentManager(),
playlistId,
selectedItem.getName()
);
}
}
@Override
public void held(PlaylistInfoItem selectedItem) {
if (selectedItem instanceof LocalPlaylistInfoItem) {
showPlaylistDialog((LocalPlaylistInfoItem) selectedItem);
}
}
});
watchHistoryButton.setOnClickListener(view -> {
if (getParentFragment() != null) {
NavigationHelper.openWatchHistoryFragment(getParentFragment().getFragmentManager());
}
});
mostWatchedButton.setOnClickListener(view -> {
if (getParentFragment() != null) {
NavigationHelper.openMostPlayedFragment(getParentFragment().getFragmentManager());
}
});
}
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:
final Toast deleteSuccessful =
Toast.makeText(getContext(), "Deleted", Toast.LENGTH_SHORT);
disposables.add(localPlaylistManager.deletePlaylist(item.getPlaylistId())
.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();
}
private void resetFragment() {
if (disposables != null) disposables.clear();
if (infoListAdapter != null) infoListAdapter.clearStreamItemList();
}
///////////////////////////////////////////////////////////////////////////
// Subscriptions Loader
///////////////////////////////////////////////////////////////////////////
@Override
public void startLoading(boolean forceLoad) {
super.startLoading(forceLoad);
resetFragment();
localPlaylistManager.getPlaylists()
.observeOn(AndroidSchedulers.mainThread())
.subscribe(getSubscriptionSubscriber());
}
private Subscriber<List<PlaylistMetadataEntry>> getSubscriptionSubscriber() {
return new Subscriber<List<PlaylistMetadataEntry>>() {
@Override
public void onSubscribe(Subscription s) {
showLoading();
if (databaseSubscription != null) databaseSubscription.cancel();
databaseSubscription = s;
databaseSubscription.request(1);
}
@Override
public void onNext(List<PlaylistMetadataEntry> subscriptions) {
handleResult(subscriptions);
if (databaseSubscription != null) databaseSubscription.request(1);
}
@Override
public void onError(Throwable exception) {
BookmarkFragment.this.onError(exception);
}
@Override
public void onComplete() {
}
};
}
@Override
public void handleResult(@NonNull List<PlaylistMetadataEntry> result) {
super.handleResult(result);
infoListAdapter.clearStreamItemList();
if (result.isEmpty()) {
showEmptyState();
} else {
infoListAdapter.addInfoItemList(infoItemsOf(result));
if (itemsListState != null) {
itemsList.getLayoutManager().onRestoreInstanceState(itemsListState);
itemsListState = null;
}
hideLoading();
}
}
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;
}
/*//////////////////////////////////////////////////////////////////////////
// Contract
//////////////////////////////////////////////////////////////////////////*/
@Override
public void showLoading() {
super.showLoading();
animateView(itemsList, false, 100);
}
@Override
public void hideLoading() {
super.hideLoading();
animateView(itemsList, true, 200);
}
@Override
public void showEmptyState() {
super.showEmptyState();
}
///////////////////////////////////////////////////////////////////////////
// Fragment Error Handling
///////////////////////////////////////////////////////////////////////////
@Override
protected boolean onError(Throwable exception) {
resetFragment();
if (super.onError(exception)) return true;
onUnrecoverableError(exception, UserAction.SOMETHING_ELSE,
"none", "Bookmark", R.string.general_error);
return true;
}
}

View file

@ -0,0 +1,323 @@
package org.schabi.newpipe.fragments.local;
import android.app.Activity;
import android.content.Context;
import android.content.DialogInterface;
import android.os.Bundle;
import android.os.Parcelable;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription;
import org.schabi.newpipe.NewPipeDatabase;
import org.schabi.newpipe.R;
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.stored.StreamStatisticsInfoItem;
import org.schabi.newpipe.playlist.PlayQueue;
import org.schabi.newpipe.playlist.SinglePlayQueue;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.NavigationHelper;
import java.util.ArrayList;
import java.util.List;
import icepick.State;
import io.reactivex.android.schedulers.AndroidSchedulers;
import static org.schabi.newpipe.util.AnimationUtils.animateView;
public abstract class HistoryPlaylistFragment
extends BaseListFragment<List<StreamStatisticsEntry>, Void> {
private View headerRootLayout;
private View playlistControl;
private View headerPlayAllButton;
private View headerPopupButton;
private View headerBackgroundButton;
@State
protected Parcelable itemsListState;
/* Used for independent events */
private Subscription databaseSubscription;
private StreamRecordManager recordManager;
///////////////////////////////////////////////////////////////////////////
// Abstracts
///////////////////////////////////////////////////////////////////////////
protected abstract String getName();
protected abstract List<InfoItem> processResult(final List<StreamStatisticsEntry> results);
protected abstract String getAdditionalDetail(final StreamStatisticsInfoItem infoItem);
///////////////////////////////////////////////////////////////////////////
// Fragment LifeCycle
///////////////////////////////////////////////////////////////////////////
@Override
public void onAttach(Context context) {
super.onAttach(context);
recordManager = new StreamRecordManager(NewPipeDatabase.getInstance(context));
}
@Override
public View onCreateView(@NonNull LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_playlist, container, false);
}
@Override
public void onPause() {
super.onPause();
itemsListState = itemsList.getLayoutManager().onSaveInstanceState();
}
@Override
public void onDestroyView() {
if (databaseSubscription != null) databaseSubscription.cancel();
super.onDestroyView();
}
@Override
public void onDestroy() {
if (databaseSubscription != null) databaseSubscription.cancel();
databaseSubscription = null;
recordManager = null;
super.onDestroy();
}
///////////////////////////////////////////////////////////////////////////
// Fragment Views
///////////////////////////////////////////////////////////////////////////
@Override
protected void initViews(View rootView, Bundle savedInstanceState) {
super.initViews(rootView, savedInstanceState);
infoListAdapter.useMiniItemVariants(true);
setFragmentTitle(getName());
}
@Override
protected View getListHeader() {
headerRootLayout = activity.getLayoutInflater().inflate(R.layout.playlist_control,
itemsList, false);
playlistControl = headerRootLayout.findViewById(R.id.playlist_control);
headerPlayAllButton = headerRootLayout.findViewById(R.id.playlist_ctrl_play_all_button);
headerPopupButton = headerRootLayout.findViewById(R.id.playlist_ctrl_play_popup_button);
headerBackgroundButton = headerRootLayout.findViewById(R.id.playlist_ctrl_play_bg_button);
return headerRootLayout;
}
@Override
protected void initListeners() {
super.initListeners();
infoListAdapter.setOnStreamSelectedListener(new InfoItemBuilder.OnInfoItemSelectedListener<StreamInfoItem>() {
@Override
public void selected(StreamInfoItem selectedItem) {
if (getParentFragment() == null) return;
// Requires the parent fragment to find holder for fragment replacement
NavigationHelper.openVideoDetailFragment(getParentFragment().getFragmentManager(),
selectedItem.getServiceId(), selectedItem.url, selectedItem.getName());
}
@Override
public void held(StreamInfoItem selectedItem) {
showStreamDialog(selectedItem);
}
});
}
@Override
protected void showStreamDialog(final StreamInfoItem item) {
final Context context = getContext();
final Activity activity = getActivity();
if (context == null || context.getResources() == null
|| getActivity() == null || !(item instanceof StreamStatisticsInfoItem)) return;
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),
};
final DialogInterface.OnClickListener actions = (dialogInterface, i) -> {
final int index = Math.max(infoListAdapter.getItemsList().indexOf(item), 0);
switch (i) {
case 0:
NavigationHelper.enqueueOnBackgroundPlayer(context, new SinglePlayQueue(item));
break;
case 1:
NavigationHelper.enqueueOnPopupPlayer(activity, new SinglePlayQueue(item));
break;
case 2:
NavigationHelper.playOnMainPlayer(context, getPlayQueue(index));
break;
case 3:
NavigationHelper.playOnBackgroundPlayer(context, getPlayQueue(index));
break;
case 4:
NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(index));
break;
default:
break;
}
};
final String detail = getAdditionalDetail((StreamStatisticsInfoItem) item);
new InfoItemDialog(getActivity(), commands, actions, item.getName(), detail).show();
}
private void resetFragment() {
if (databaseSubscription != null) databaseSubscription.cancel();
if (infoListAdapter != null) infoListAdapter.clearStreamItemList();
}
///////////////////////////////////////////////////////////////////////////
// Loader
///////////////////////////////////////////////////////////////////////////
@Override
public void showLoading() {
super.showLoading();
animateView(headerRootLayout, false, 200);
animateView(itemsList, false, 100);
}
@Override
public void startLoading(boolean forceLoad) {
super.startLoading(forceLoad);
resetFragment();
recordManager.getStatistics()
.observeOn(AndroidSchedulers.mainThread())
.subscribe(getHistoryObserver());
}
private Subscriber<List<StreamStatisticsEntry>> getHistoryObserver() {
return new Subscriber<List<StreamStatisticsEntry>>() {
@Override
public void onSubscribe(Subscription s) {
showLoading();
if (databaseSubscription != null) databaseSubscription.cancel();
databaseSubscription = s;
databaseSubscription.request(1);
}
@Override
public void onNext(List<StreamStatisticsEntry> streams) {
handleResult(streams);
if (databaseSubscription != null) databaseSubscription.request(1);
}
@Override
public void onError(Throwable exception) {
HistoryPlaylistFragment.this.onError(exception);
}
@Override
public void onComplete() {
}
};
}
@Override
public void handleResult(@NonNull List<StreamStatisticsEntry> result) {
super.handleResult(result);
infoListAdapter.clearStreamItemList();
if (result.isEmpty()) {
showEmptyState();
return;
}
animateView(headerRootLayout, true, 100);
animateView(itemsList, true, 300);
infoListAdapter.addInfoItemList(processResult(result));
if (itemsListState != null) {
itemsList.getLayoutManager().onRestoreInstanceState(itemsListState);
itemsListState = null;
}
playlistControl.setVisibility(View.VISIBLE);
headerPlayAllButton.setOnClickListener(view ->
NavigationHelper.playOnMainPlayer(activity, getPlayQueue()));
headerPopupButton.setOnClickListener(view ->
NavigationHelper.playOnPopupPlayer(activity, getPlayQueue()));
headerBackgroundButton.setOnClickListener(view ->
NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue()));
hideLoading();
}
/*//////////////////////////////////////////////////////////////////////////
// Contract
//////////////////////////////////////////////////////////////////////////*/
@Override
protected void loadMoreItems() {
// Do nothing
}
@Override
protected boolean hasMoreItems() {
return false;
}
///////////////////////////////////////////////////////////////////////////
// Fragment Error Handling
///////////////////////////////////////////////////////////////////////////
@Override
protected boolean onError(Throwable exception) {
resetFragment();
if (super.onError(exception)) return true;
onUnrecoverableError(exception, UserAction.SOMETHING_ELSE,
"none", "History", R.string.general_error);
return true;
}
/*//////////////////////////////////////////////////////////////////////////
// Utils
//////////////////////////////////////////////////////////////////////////*/
protected void setFragmentTitle(final String title) {
if (activity.getSupportActionBar() != null) {
activity.getSupportActionBar().setTitle(title);
}
}
private PlayQueue getPlayQueue() {
return getPlayQueue(0);
}
private PlayQueue getPlayQueue(final int index) {
final List<InfoItem> infoItems = infoListAdapter.getItemsList();
List<StreamInfoItem> streamInfoItems = new ArrayList<>(infoItems.size());
for (final InfoItem item : infoItems) {
if (item instanceof StreamInfoItem) streamInfoItems.add((StreamInfoItem) item);
}
return new SinglePlayQueue(streamInfoItems, index);
}
}

View file

@ -0,0 +1,356 @@
package org.schabi.newpipe.fragments.local;
import android.app.Activity;
import android.content.Context;
import android.content.DialogInterface;
import android.os.Bundle;
import android.os.Parcelable;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription;
import org.schabi.newpipe.NewPipeDatabase;
import org.schabi.newpipe.R;
import org.schabi.newpipe.database.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.playlist.PlayQueue;
import org.schabi.newpipe.playlist.SinglePlayQueue;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.NavigationHelper;
import java.util.ArrayList;
import java.util.List;
import icepick.State;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable;
import static org.schabi.newpipe.util.AnimationUtils.animateView;
public class LocalPlaylistFragment extends BaseListFragment<List<StreamEntity>, Void> {
private View headerRootLayout;
private TextView headerTitleView;
private TextView headerStreamCount;
private View playlistControl;
private View headerPlayAllButton;
private View headerPopupButton;
private View headerBackgroundButton;
@State
protected long playlistId;
@State
protected String name;
@State
protected Parcelable itemsListState;
/* Used for independent events */
private CompositeDisposable disposables = new CompositeDisposable();
private Subscription databaseSubscription;
private LocalPlaylistManager playlistManager;
public static LocalPlaylistFragment getInstance(long playlistId, String name) {
LocalPlaylistFragment instance = new LocalPlaylistFragment();
instance.setInitialData(playlistId, name);
return instance;
}
///////////////////////////////////////////////////////////////////////////
// Fragment LifeCycle
///////////////////////////////////////////////////////////////////////////
@Override
public void onAttach(Context context) {
super.onAttach(context);
playlistManager = new LocalPlaylistManager(NewPipeDatabase.getInstance(context));
}
@Override
public View onCreateView(@NonNull LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_playlist, container, false);
}
@Override
public void onPause() {
super.onPause();
itemsListState = itemsList.getLayoutManager().onSaveInstanceState();
}
@Override
public void onDestroyView() {
if (disposables != null) disposables.clear();
super.onDestroyView();
}
@Override
public void onDestroy() {
if (disposables != null) disposables.dispose();
if (databaseSubscription != null) databaseSubscription.cancel();
disposables = null;
databaseSubscription = null;
playlistManager = null;
super.onDestroy();
}
///////////////////////////////////////////////////////////////////////////
// Fragment Views
///////////////////////////////////////////////////////////////////////////
@Override
protected void initViews(View rootView, Bundle savedInstanceState) {
super.initViews(rootView, savedInstanceState);
infoListAdapter.useMiniItemVariants(true);
setFragmentTitle(name);
}
@Override
protected View getListHeader() {
headerRootLayout = activity.getLayoutInflater().inflate(R.layout.local_playlist_header,
itemsList, false);
headerTitleView = headerRootLayout.findViewById(R.id.playlist_title_view);
headerTitleView.setSelected(true);
headerStreamCount = headerRootLayout.findViewById(R.id.playlist_stream_count);
playlistControl = headerRootLayout.findViewById(R.id.playlist_control);
headerPlayAllButton = headerRootLayout.findViewById(R.id.playlist_ctrl_play_all_button);
headerPopupButton = headerRootLayout.findViewById(R.id.playlist_ctrl_play_popup_button);
headerBackgroundButton = headerRootLayout.findViewById(R.id.playlist_ctrl_play_bg_button);
return headerRootLayout;
}
@Override
protected void initListeners() {
super.initListeners();
infoListAdapter.setOnStreamSelectedListener(new InfoItemBuilder.OnInfoItemSelectedListener<StreamInfoItem>() {
@Override
public void selected(StreamInfoItem selectedItem) {
if (getParentFragment() == null) return;
// Requires the parent fragment to find holder for fragment replacement
NavigationHelper.openVideoDetailFragment(getParentFragment().getFragmentManager(),
selectedItem.getServiceId(), selectedItem.url, selectedItem.getName());
}
@Override
public void held(StreamInfoItem selectedItem) {
showStreamDialog(selectedItem);
}
});
}
@Override
protected void showStreamDialog(final StreamInfoItem item) {
final Context context = getContext();
final Activity activity = getActivity();
if (context == null || context.getResources() == null || getActivity() == null) return;
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),
};
final DialogInterface.OnClickListener actions = (dialogInterface, i) -> {
final int index = Math.max(infoListAdapter.getItemsList().indexOf(item), 0);
switch (i) {
case 0:
NavigationHelper.enqueueOnBackgroundPlayer(context, new SinglePlayQueue(item));
break;
case 1:
NavigationHelper.enqueueOnPopupPlayer(activity, new SinglePlayQueue(item));
break;
case 2:
NavigationHelper.playOnMainPlayer(context, getPlayQueue(index));
break;
case 3:
NavigationHelper.playOnBackgroundPlayer(context, getPlayQueue(index));
break;
case 4:
NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(index));
break;
default:
break;
}
};
new InfoItemDialog(getActivity(), item, commands, actions).show();
}
private void resetFragment() {
if (disposables != null) disposables.clear();
if (databaseSubscription != null) databaseSubscription.cancel();
if (infoListAdapter != null) infoListAdapter.clearStreamItemList();
}
///////////////////////////////////////////////////////////////////////////
// Loader
///////////////////////////////////////////////////////////////////////////
@Override
public void showLoading() {
super.showLoading();
animateView(headerRootLayout, false, 200);
animateView(itemsList, false, 100);
}
@Override
public void startLoading(boolean forceLoad) {
super.startLoading(forceLoad);
resetFragment();
playlistManager.getPlaylist(playlistId)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(getPlaylistObserver());
}
private Subscriber<List<StreamEntity>> getPlaylistObserver() {
return new Subscriber<List<StreamEntity>>() {
@Override
public void onSubscribe(Subscription s) {
showLoading();
if (databaseSubscription != null) databaseSubscription.cancel();
databaseSubscription = s;
databaseSubscription.request(1);
}
@Override
public void onNext(List<StreamEntity> streams) {
handleResult(streams);
if (databaseSubscription != null) databaseSubscription.request(1);
}
@Override
public void onError(Throwable exception) {
LocalPlaylistFragment.this.onError(exception);
}
@Override
public void onComplete() {
}
};
}
@Override
public void handleResult(@NonNull List<StreamEntity> result) {
super.handleResult(result);
infoListAdapter.clearStreamItemList();
if (result.isEmpty()) {
showEmptyState();
return;
}
animateView(headerRootLayout, true, 100);
animateView(itemsList, true, 300);
infoListAdapter.addInfoItemList(getStreamItems(result));
if (itemsListState != null) {
itemsList.getLayoutManager().onRestoreInstanceState(itemsListState);
itemsListState = null;
}
playlistControl.setVisibility(View.VISIBLE);
headerStreamCount.setText(
getResources().getQuantityString(R.plurals.videos, result.size(), result.size()));
headerPlayAllButton.setOnClickListener(view ->
NavigationHelper.playOnMainPlayer(activity, getPlayQueue()));
headerPopupButton.setOnClickListener(view ->
NavigationHelper.playOnPopupPlayer(activity, getPlayQueue()));
headerBackgroundButton.setOnClickListener(view ->
NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue()));
hideLoading();
}
private List<InfoItem> getStreamItems(final List<StreamEntity> streams) {
List<InfoItem> items = new ArrayList<>(streams.size());
for (final StreamEntity stream : streams) {
items.add(stream.toStreamInfoItem());
}
return items;
}
/*//////////////////////////////////////////////////////////////////////////
// Contract
//////////////////////////////////////////////////////////////////////////*/
@Override
protected void loadMoreItems() {
// Do nothing
}
@Override
protected boolean hasMoreItems() {
return false;
}
///////////////////////////////////////////////////////////////////////////
// Fragment Error Handling
///////////////////////////////////////////////////////////////////////////
@Override
protected boolean onError(Throwable exception) {
resetFragment();
if (super.onError(exception)) return true;
onUnrecoverableError(exception, UserAction.SOMETHING_ELSE,
"none", "Subscriptions", R.string.general_error);
return true;
}
/*//////////////////////////////////////////////////////////////////////////
// Utils
//////////////////////////////////////////////////////////////////////////*/
protected 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) {
activity.getSupportActionBar().setTitle(title);
}
if (headerTitleView != null) {
headerTitleView.setText(title);
}
}
private PlayQueue getPlayQueue() {
return getPlayQueue(0);
}
private PlayQueue getPlayQueue(final int index) {
final List<InfoItem> infoItems = infoListAdapter.getItemsList();
List<StreamInfoItem> streamInfoItems = new ArrayList<>(infoItems.size());
for (final InfoItem item : infoItems) {
if (item instanceof StreamInfoItem) streamInfoItems.add((StreamInfoItem) item);
}
return new SinglePlayQueue(streamInfoItems, index);
}
}

View file

@ -1,4 +1,4 @@
package org.schabi.newpipe.fragments.playlist; package org.schabi.newpipe.fragments.local;
import org.schabi.newpipe.database.AppDatabase; import org.schabi.newpipe.database.AppDatabase;
import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry; import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry;
@ -13,8 +13,9 @@ import java.util.ArrayList;
import java.util.List; import java.util.List;
import io.reactivex.Completable; import io.reactivex.Completable;
import io.reactivex.Flowable;
import io.reactivex.Maybe; import io.reactivex.Maybe;
import io.reactivex.Scheduler; import io.reactivex.Single;
import io.reactivex.schedulers.Schedulers; import io.reactivex.schedulers.Schedulers;
public class LocalPlaylistManager { public class LocalPlaylistManager {
@ -74,9 +75,16 @@ public class LocalPlaylistManager {
})); }));
} }
public Maybe<List<PlaylistMetadataEntry>> getPlaylists() { public Flowable<List<PlaylistMetadataEntry>> getPlaylists() {
return playlistStreamTable.getPlaylistMetadata() return playlistStreamTable.getPlaylistMetadata().subscribeOn(Schedulers.io());
.firstElement() }
public Flowable<List<StreamEntity>> getPlaylist(final long playlistId) {
return playlistStreamTable.getOrderedStreamsOf(playlistId).subscribeOn(Schedulers.io());
}
public Single<Integer> deletePlaylist(final long playlistId) {
return Single.fromCallable(() -> playlistTable.deletePlaylist(playlistId))
.subscribeOn(Schedulers.io()); .subscribeOn(Schedulers.io());
} }
} }

View file

@ -0,0 +1,35 @@
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 HistoryPlaylistFragment {
@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

@ -1,4 +1,4 @@
package org.schabi.newpipe.fragments.playlist; package org.schabi.newpipe.fragments.local;
import android.content.Context; import android.content.Context;
import android.os.Bundle; import android.os.Bundle;
@ -113,6 +113,7 @@ public class PlaylistAppendDialog extends DialogFragment {
.subscribe(metadataEntries -> { .subscribe(metadataEntries -> {
if (metadataEntries.isEmpty()) { if (metadataEntries.isEmpty()) {
openCreatePlaylistDialog(); openCreatePlaylistDialog();
return;
} }
List<InfoItem> playlistInfoItems = new ArrayList<>(metadataEntries.size()); List<InfoItem> playlistInfoItems = new ArrayList<>(metadataEntries.size());
@ -123,8 +124,6 @@ public class PlaylistAppendDialog extends DialogFragment {
playlistAdapter.clearStreamItemList(); playlistAdapter.clearStreamItemList();
playlistAdapter.addInfoItemList(playlistInfoItems); playlistAdapter.addInfoItemList(playlistInfoItems);
playlistRecyclerView.setVisibility(View.VISIBLE); playlistRecyclerView.setVisibility(View.VISIBLE);
getDialog().setCanceledOnTouchOutside(true);
}); });
} }
@ -141,7 +140,7 @@ public class PlaylistAppendDialog extends DialogFragment {
public void openCreatePlaylistDialog() { public void openCreatePlaylistDialog() {
if (streamInfo == null || getFragmentManager() == null) return; if (streamInfo == null || getFragmentManager() == null) return;
getDialog().dismiss();
PlaylistCreationDialog.newInstance(streamInfo).show(getFragmentManager(), TAG); PlaylistCreationDialog.newInstance(streamInfo).show(getFragmentManager(), TAG);
getDialog().dismiss();
} }
} }

View file

@ -1,4 +1,4 @@
package org.schabi.newpipe.fragments.playlist; package org.schabi.newpipe.fragments.local;
import android.app.AlertDialog; import android.app.AlertDialog;
import android.app.Dialog; import android.app.Dialog;

View file

@ -1,4 +1,4 @@
package org.schabi.newpipe.fragments.playlist; package org.schabi.newpipe.fragments.local;
import org.schabi.newpipe.database.AppDatabase; import org.schabi.newpipe.database.AppDatabase;
import org.schabi.newpipe.database.stream.StreamStatisticsEntry; import org.schabi.newpipe.database.stream.StreamStatisticsEntry;
@ -7,14 +7,12 @@ import org.schabi.newpipe.database.stream.dao.StreamHistoryDAO;
import org.schabi.newpipe.database.stream.model.StreamEntity; import org.schabi.newpipe.database.stream.model.StreamEntity;
import org.schabi.newpipe.database.stream.model.StreamHistoryEntity; import org.schabi.newpipe.database.stream.model.StreamHistoryEntity;
import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
import io.reactivex.MaybeObserver; import io.reactivex.Flowable;
import io.reactivex.Single; import io.reactivex.Single;
import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers; import io.reactivex.schedulers.Schedulers;
public class StreamRecordManager { public class StreamRecordManager {
@ -29,11 +27,6 @@ public class StreamRecordManager {
historyTable = db.streamHistoryDAO(); historyTable = db.streamHistoryDAO();
} }
public int onChanged(final StreamInfoItem infoItem) {
// Only existing streams are updated
return streamTable.update(new StreamEntity(infoItem));
}
public Single<Long> onViewed(final StreamInfo info) { public Single<Long> onViewed(final StreamInfo info) {
return Single.fromCallable(() -> database.runInTransaction(() -> { return Single.fromCallable(() -> database.runInTransaction(() -> {
final long streamId = streamTable.upsert(new StreamEntity(info)); final long streamId = streamTable.upsert(new StreamEntity(info));
@ -45,30 +38,7 @@ public class StreamRecordManager {
return historyTable.deleteHistory(streamId); return historyTable.deleteHistory(streamId);
} }
public void removeRecord() { public Flowable<List<StreamStatisticsEntry>> getStatistics() {
historyTable.getStatistics().firstElement().subscribe( return historyTable.getStatistics();
new MaybeObserver<List<StreamStatisticsEntry>>() {
@Override
public void onSubscribe(Disposable d) {
}
@Override
public void onSuccess(List<StreamStatisticsEntry> streamStatisticsEntries) {
hashCode();
}
@Override
public void onError(Throwable e) {
}
@Override
public void onComplete() {
}
}
);
} }
} }

View file

@ -0,0 +1,36 @@
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 HistoryPlaylistFragment {
@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

@ -47,6 +47,14 @@ public class PlaylistMiniInfoItemHolder extends InfoItemHolder {
itemBuilder.getOnPlaylistSelectedListener().selected(item); itemBuilder.getOnPlaylistSelectedListener().selected(item);
} }
}); });
itemView.setLongClickable(true);
itemView.setOnLongClickListener(view -> {
if (itemBuilder.getOnPlaylistSelectedListener() != null) {
itemBuilder.getOnPlaylistSelectedListener().held(item);
}
return true;
});
} }
/** /**

View file

@ -64,7 +64,7 @@ import com.nostra13.universalimageloader.core.listener.SimpleImageLoadingListene
import org.schabi.newpipe.NewPipeDatabase; import org.schabi.newpipe.NewPipeDatabase;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.fragments.playlist.StreamRecordManager; import org.schabi.newpipe.fragments.local.StreamRecordManager;
import org.schabi.newpipe.player.helper.AudioReactor; import org.schabi.newpipe.player.helper.AudioReactor;
import org.schabi.newpipe.player.helper.CacheFactory; import org.schabi.newpipe.player.helper.CacheFactory;
import org.schabi.newpipe.player.helper.LoadController; import org.schabi.newpipe.player.helper.LoadController;
@ -676,7 +676,6 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen
} }
databaseUpdateReactor.add(recordManager.onViewed(currentInfo).subscribe()); databaseUpdateReactor.add(recordManager.onViewed(currentInfo).subscribe());
recordManager.removeRecord();
initThumbnail(info == null ? item.getThumbnailUrl() : info.thumbnail_url); initThumbnail(info == null ? item.getThumbnailUrl() : info.thumbnail_url);
} }

View file

@ -3,19 +3,29 @@ package org.schabi.newpipe.playlist;
import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List;
public final class SinglePlayQueue extends PlayQueue { public final class SinglePlayQueue extends PlayQueue {
public SinglePlayQueue(final StreamInfoItem item) { public SinglePlayQueue(final StreamInfoItem item) {
this(new PlayQueueItem(item)); super(0, Collections.singletonList(new PlayQueueItem(item)));
} }
public SinglePlayQueue(final StreamInfo info) { public SinglePlayQueue(final StreamInfo info) {
this(new PlayQueueItem(info)); super(0, Collections.singletonList(new PlayQueueItem(info)));
} }
private SinglePlayQueue(final PlayQueueItem playQueueItem) { public SinglePlayQueue(final List<StreamInfoItem> items, final int index) {
super(0, Collections.singletonList(playQueueItem)); super(index, playQueueItemsOf(items));
}
private static List<PlayQueueItem> playQueueItemsOf(List<StreamInfoItem> items) {
List<PlayQueueItem> playQueueItems = new ArrayList<>(items.size());
for (final StreamInfoItem item : items) {
playQueueItems.add(new PlayQueueItem(item));
}
return playQueueItems;
} }
@Override @Override

View file

@ -34,6 +34,9 @@ import org.schabi.newpipe.fragments.list.feed.FeedFragment;
import org.schabi.newpipe.fragments.list.kiosk.KioskFragment; import org.schabi.newpipe.fragments.list.kiosk.KioskFragment;
import org.schabi.newpipe.fragments.list.playlist.PlaylistFragment; import org.schabi.newpipe.fragments.list.playlist.PlaylistFragment;
import org.schabi.newpipe.fragments.list.search.SearchFragment; 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.history.HistoryActivity; import org.schabi.newpipe.history.HistoryActivity;
import org.schabi.newpipe.player.BackgroundPlayer; import org.schabi.newpipe.player.BackgroundPlayer;
import org.schabi.newpipe.player.BackgroundPlayerActivity; import org.schabi.newpipe.player.BackgroundPlayerActivity;
@ -323,6 +326,30 @@ public class NavigationHelper {
.commit(); .commit();
} }
public static void openLocalPlaylistFragment(FragmentManager fragmentManager, long playlistId, String name) {
if (name == null) name = "";
fragmentManager.beginTransaction()
.setCustomAnimations(R.animator.custom_fade_in, R.animator.custom_fade_out, R.animator.custom_fade_in, R.animator.custom_fade_out)
.replace(R.id.fragment_holder, LocalPlaylistFragment.getInstance(playlistId, name))
.addToBackStack(null)
.commit();
}
public static void openWatchHistoryFragment(FragmentManager fragmentManager) {
fragmentManager.beginTransaction()
.setCustomAnimations(R.animator.custom_fade_in, R.animator.custom_fade_out, R.animator.custom_fade_in, R.animator.custom_fade_out)
.replace(R.id.fragment_holder, new WatchHistoryFragment())
.addToBackStack(null)
.commit();
}
public static void openMostPlayedFragment(FragmentManager fragmentManager) {
fragmentManager.beginTransaction()
.setCustomAnimations(R.animator.custom_fade_in, R.animator.custom_fade_out, R.animator.custom_fade_in, R.animator.custom_fade_out)
.replace(R.id.fragment_holder, new MostPlayedFragment())
.addToBackStack(null)
.commit();
}
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// Through Intents // Through Intents
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/

View file

@ -0,0 +1,81 @@
<?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:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
android:background="?attr/selectableItemBackground">
<RelativeLayout
android:id="@+id/watchHistory"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:focusable="true">
<ImageView
android:id="@+id/watchHistoryIcon"
android:layout_width="48dp"
android:layout_height="28dp"
android:layout_alignParentLeft="true"
android:layout_centerVertical="true"
android:layout_marginLeft="12dp"
android:layout_marginRight="12dp"
android:src="?attr/history"
tools:ignore="ContentDescription,RtlHardcoded"/>
<TextView
android:id="@+id/watchHistoryText"
android:layout_width="match_parent"
android:layout_height="50dp"
android:layout_toRightOf="@+id/watchHistoryIcon"
android:gravity="left|center"
android:text="@string/title_watch_history"
android:textAppearance="?android:attr/textAppearanceLarge"
android:textSize="15sp"
android:textStyle="bold"
tools:ignore="RtlHardcoded"/>
</RelativeLayout>
<RelativeLayout
android:id="@+id/mostWatched"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/watchHistory"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:focusable="true">
<ImageView
android:id="@+id/mostWatchedIcon"
android:layout_width="48dp"
android:layout_height="28dp"
android:layout_alignParentLeft="true"
android:layout_centerVertical="true"
android:layout_marginLeft="12dp"
android:layout_marginRight="12dp"
android:src="?attr/filter"
tools:ignore="ContentDescription,RtlHardcoded"/>
<TextView
android:id="@+id/mostWatchedText"
android:layout_width="match_parent"
android:layout_height="50dp"
android:layout_toRightOf="@+id/mostWatchedIcon"
android:gravity="left|center"
android:text="@string/title_most_played"
android:textAppearance="?android:attr/textAppearanceLarge"
android:textSize="15sp"
android:textStyle="bold"
tools:ignore="RtlHardcoded"/>
</RelativeLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_below="@+id/mostWatched"
android:layout_marginLeft="8dp"
android:layout_marginRight="8dp"
android:background="?attr/separator_color"/>
</RelativeLayout>

View file

@ -0,0 +1,44 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.v7.widget.RecyclerView
android:id="@+id/items_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollbars="vertical"
app:layoutManager="LinearLayoutManager"
tools:listitem="@layout/list_playlist_mini_item"/>
<!--ERROR PANEL-->
<include
android:id="@+id/error_panel"
layout="@layout/error_retry"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_marginTop="50dp"
android:visibility="gone"
tools:visibility="visible"/>
<include
android:id="@+id/empty_state_view"
layout="@layout/list_empty_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:layout_marginTop="50dp"
android:visibility="gone"
tools:visibility="visible"/>
<View
android:layout_width="match_parent"
android:layout_height="4dp"
android:background="?attr/toolbar_shadow_drawable"
android:layout_alignParentTop="true"/>
</RelativeLayout>

View file

@ -26,7 +26,7 @@
<include <include
android:id="@+id/empty_state_view" android:id="@+id/empty_state_view"
layout="@layout/subscription_feed_empty_view" layout="@layout/list_empty_view"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_centerInParent="true" android:layout_centerInParent="true"

View file

@ -27,7 +27,7 @@
<include <include
android:id="@+id/empty_state_view" android:id="@+id/empty_state_view"
layout="@layout/subscription_feed_empty_view" layout="@layout/list_empty_view"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_centerInParent="true" android:layout_centerInParent="true"

View file

@ -19,7 +19,7 @@
android:layout_alignParentTop="true" android:layout_alignParentTop="true"
android:layout_marginRight="@dimen/video_item_search_image_right_margin" android:layout_marginRight="@dimen/video_item_search_image_right_margin"
android:contentDescription="@string/list_thumbnail_view_description" android:contentDescription="@string/list_thumbnail_view_description"
android:scaleType="fitEnd" android:scaleType="centerCrop"
android:src="@drawable/dummy_thumbnail_playlist" android:src="@drawable/dummy_thumbnail_playlist"
tools:ignore="RtlHardcoded"/> tools:ignore="RtlHardcoded"/>

View file

@ -0,0 +1,48 @@
<?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:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/contrast_background_color">
<TextView
android:id="@+id/playlist_title_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="8dp"
android:layout_marginRight="8dp"
android:layout_marginTop="6dp"
android:ellipsize="marquee"
android:fadingEdge="horizontal"
android:marqueeRepeatLimit="marquee_forever"
android:scrollHorizontally="true"
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" />
<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_alignParentRight="true"
android:layout_marginRight="6dp"
android:ellipsize="end"
android:gravity="right|center_vertical"
android:maxLines="1"
android:textSize="@dimen/playlist_detail_subtext_size"
tools:ignore="RtlHardcoded"
tools:text="234 videos"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/playlist_stream_count">
<include layout="@layout/playlist_control"/>
</LinearLayout>
</RelativeLayout>

View file

@ -119,12 +119,14 @@
<string name="subscription_page_key" translatable="false">subscription_page_key</string> <string name="subscription_page_key" translatable="false">subscription_page_key</string>
<string name="kiosk_page_key" translatable="false">kiosk_page</string> <string name="kiosk_page_key" translatable="false">kiosk_page</string>
<string name="channel_page_key" translatable="false">channel_page</string> <string name="channel_page_key" translatable="false">channel_page</string>
<string name="bookmark_page_key" translatable="false">bookmark_page</string>
<string-array name="main_page_content_pages" translatable="false"> <string-array name="main_page_content_pages" translatable="false">
<item>@string/blank_page_key</item> <item>@string/blank_page_key</item>
<item>@string/kiosk_page_key</item> <item>@string/kiosk_page_key</item>
<item>@string/feed_page_key</item> <item>@string/feed_page_key</item>
<item>@string/subscription_page_key</item> <item>@string/subscription_page_key</item>
<item>@string/channel_page_key</item> <item>@string/channel_page_key</item>
<item>@string/bookmark_page_key</item>
</string-array> </string-array>
<string name="main_page_selected_service" translatable="false">main_page_selected_service</string> <string name="main_page_selected_service" translatable="false">main_page_selected_service</string>
<string name="main_page_selected_channel_name" translatable="false">main_page_selected_channel_name</string> <string name="main_page_selected_channel_name" translatable="false">main_page_selected_channel_name</string>

View file

@ -32,6 +32,7 @@
<string name="tab_main">Main</string> <string name="tab_main">Main</string>
<string name="tab_subscriptions">Subscriptions</string> <string name="tab_subscriptions">Subscriptions</string>
<string name="tab_bookmarks">Bookmarks</string>
<string name="fragment_whats_new">What\'s New</string> <string name="fragment_whats_new">What\'s New</string>
@ -304,6 +305,8 @@
<string name="history_cleared">History cleared</string> <string name="history_cleared">History cleared</string>
<string name="item_deleted">Item deleted</string> <string name="item_deleted">Item deleted</string>
<string name="delete_item_search_history">Do you want to delete this item from search history?</string> <string name="delete_item_search_history">Do you want to delete this item from search history?</string>
<string name="title_watch_history">Watch History</string>
<string name="title_most_played">Most Played</string>
<!-- Content --> <!-- Content -->
<string name="main_page_content">Content of main page</string> <string name="main_page_content">Content of main page</string>
@ -370,5 +373,6 @@
<!-- Local Playlist --> <!-- Local Playlist -->
<string name="create_playlist">Create New Playlist</string> <string name="create_playlist">Create New Playlist</string>
<string name="delete_playlist">Delete Playlist</string>
<string name="playlist_name_input">Name</string> <string name="playlist_name_input">Name</string>
</resources> </resources>