diff --git a/app/src/main/java/org/schabi/newpipe/database/AppDatabase.java b/app/src/main/java/org/schabi/newpipe/database/AppDatabase.java index e09687ce4..dedbfbf68 100644 --- a/app/src/main/java/org/schabi/newpipe/database/AppDatabase.java +++ b/app/src/main/java/org/schabi/newpipe/database/AppDatabase.java @@ -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.playlist.model.PlaylistEntity; 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.StreamEntity; +import org.schabi.newpipe.database.stream.model.StreamStateEntity; import org.schabi.newpipe.database.subscription.SubscriptionDAO; import org.schabi.newpipe.database.subscription.SubscriptionEntity; @@ -23,8 +25,8 @@ import org.schabi.newpipe.database.subscription.SubscriptionEntity; @Database( entities = { SubscriptionEntity.class, WatchHistoryEntry.class, SearchHistoryEntry.class, - StreamEntity.class, StreamHistoryEntity.class, PlaylistEntity.class, - PlaylistStreamEntity.class + StreamEntity.class, StreamHistoryEntity.class, StreamStateEntity.class, + PlaylistEntity.class, PlaylistStreamEntity.class }, version = 1, exportSchema = false @@ -43,6 +45,8 @@ public abstract class AppDatabase extends RoomDatabase { public abstract StreamHistoryDAO streamHistoryDAO(); + public abstract StreamStateDAO streamStateDAO(); + public abstract PlaylistDAO playlistDAO(); public abstract PlaylistStreamDAO playlistStreamDAO(); diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistDAO.java b/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistDAO.java index b337769bc..88d5645af 100644 --- a/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistDAO.java +++ b/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistDAO.java @@ -32,4 +32,7 @@ public abstract class PlaylistDAO implements BasicDAO { @Query("SELECT * FROM " + PLAYLIST_TABLE + " WHERE " + PLAYLIST_ID + " = :playlistId") public abstract Flowable> getPlaylist(final long playlistId); + + @Query("DELETE FROM " + PLAYLIST_TABLE + " WHERE " + PLAYLIST_ID + " = :playlistId") + public abstract int deletePlaylist(final long playlistId); } diff --git a/app/src/main/java/org/schabi/newpipe/database/stream/StreamStatisticsEntry.java b/app/src/main/java/org/schabi/newpipe/database/stream/StreamStatisticsEntry.java index 5893394c5..722cff5cd 100644 --- a/app/src/main/java/org/schabi/newpipe/database/stream/StreamStatisticsEntry.java +++ b/app/src/main/java/org/schabi/newpipe/database/stream/StreamStatisticsEntry.java @@ -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.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; @@ -51,4 +53,15 @@ public class StreamStatisticsEntry { this.latestAccessDate = latestAccessDate; 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; + } } diff --git a/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamStateDAO.java b/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamStateDAO.java new file mode 100644 index 000000000..f89f2f7ef --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamStateDAO.java @@ -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 { + @Override + @Query("SELECT * FROM " + STREAM_STATE_TABLE) + public abstract Flowable> getAll(); + + @Override + @Query("DELETE FROM " + STREAM_STATE_TABLE) + public abstract int deleteAll(); + + @Override + public Flowable> listByService(int serviceId) { + throw new UnsupportedOperationException(); + } + + @Query("DELETE FROM " + STREAM_STATE_TABLE + " WHERE " + JOIN_STREAM_ID + " = :streamId") + public abstract int deleteState(final long streamId); +} diff --git a/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamStateEntity.java b/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamStateEntity.java new file mode 100644 index 000000000..15940a964 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamStateEntity.java @@ -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; + } +} diff --git a/app/src/main/java/org/schabi/newpipe/fragments/MainFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/MainFragment.java index 3a8c7569c..e76b97086 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/MainFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/MainFragment.java @@ -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.feed.FeedFragment; 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.report.ErrorActivity; import org.schabi.newpipe.report.UserAction; @@ -87,9 +88,11 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte if (isSubscriptionsPageOnlySelected()) { tabLayout.getTabAt(0).setIcon(channelIcon); + tabLayout.getTabAt(1).setText(R.string.tab_bookmarks); } else { tabLayout.getTabAt(0).setIcon(whatsHotIcon); 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 { - PagerAdapter(FragmentManager fm) { super(fm); } @@ -158,7 +160,15 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte case 0: return isSubscriptionsPageOnlySelected() ? new SubscriptionFragment() : getMainPageFragment(); 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: return new BlankFragment(); } @@ -172,7 +182,7 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte @Override 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() { + if (getActivity() == null) return new BlankFragment(); + try { SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getActivity()); @@ -216,6 +228,10 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte ChannelFragment fragment = ChannelFragment.getInstance(serviceId, url, name); fragment.useAsFrontPage(true); return fragment; + } else if (setMainPage.equals(getString(R.string.bookmark_page_key))) { + final BookmarkFragment fragment = new BookmarkFragment(); + fragment.useAsFrontPage(true); + return fragment; } else { return new BlankFragment(); } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/BookmarkFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/local/BookmarkFragment.java new file mode 100644 index 000000000..ecbd416ee --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/BookmarkFragment.java @@ -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> { + 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() { + @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> getSubscriptionSubscriber() { + return new Subscriber>() { + @Override + public void onSubscribe(Subscription s) { + showLoading(); + if (databaseSubscription != null) databaseSubscription.cancel(); + databaseSubscription = s; + databaseSubscription.request(1); + } + + @Override + public void onNext(List 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 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 infoItemsOf(List playlists) { + List 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; + } +} + diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/HistoryPlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/local/HistoryPlaylistFragment.java new file mode 100644 index 000000000..3941df6c0 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/HistoryPlaylistFragment.java @@ -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, 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 processResult(final List 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() { + @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> getHistoryObserver() { + return new Subscriber>() { + @Override + public void onSubscribe(Subscription s) { + showLoading(); + + if (databaseSubscription != null) databaseSubscription.cancel(); + databaseSubscription = s; + databaseSubscription.request(1); + } + + @Override + public void onNext(List 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 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 infoItems = infoListAdapter.getItemsList(); + List streamInfoItems = new ArrayList<>(infoItems.size()); + for (final InfoItem item : infoItems) { + if (item instanceof StreamInfoItem) streamInfoItems.add((StreamInfoItem) item); + } + return new SinglePlayQueue(streamInfoItems, index); + } +} + diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistFragment.java new file mode 100644 index 000000000..6709b1bad --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistFragment.java @@ -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, 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() { + @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> getPlaylistObserver() { + return new Subscriber>() { + @Override + public void onSubscribe(Subscription s) { + showLoading(); + + if (databaseSubscription != null) databaseSubscription.cancel(); + databaseSubscription = s; + databaseSubscription.request(1); + } + + @Override + public void onNext(List 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 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 getStreamItems(final List streams) { + List 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 infoItems = infoListAdapter.getItemsList(); + List streamInfoItems = new ArrayList<>(infoItems.size()); + for (final InfoItem item : infoItems) { + if (item instanceof StreamInfoItem) streamInfoItems.add((StreamInfoItem) item); + } + return new SinglePlayQueue(streamInfoItems, index); + } +} + diff --git a/app/src/main/java/org/schabi/newpipe/fragments/playlist/LocalPlaylistManager.java b/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistManager.java similarity index 84% rename from app/src/main/java/org/schabi/newpipe/fragments/playlist/LocalPlaylistManager.java rename to app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistManager.java index 911b3c7fd..bf7bc14c8 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/playlist/LocalPlaylistManager.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistManager.java @@ -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.playlist.PlaylistMetadataEntry; @@ -13,8 +13,9 @@ import java.util.ArrayList; import java.util.List; import io.reactivex.Completable; +import io.reactivex.Flowable; import io.reactivex.Maybe; -import io.reactivex.Scheduler; +import io.reactivex.Single; import io.reactivex.schedulers.Schedulers; public class LocalPlaylistManager { @@ -74,9 +75,16 @@ public class LocalPlaylistManager { })); } - public Maybe> getPlaylists() { - return playlistStreamTable.getPlaylistMetadata() - .firstElement() + public Flowable> getPlaylists() { + return playlistStreamTable.getPlaylistMetadata().subscribeOn(Schedulers.io()); + } + + public Flowable> getPlaylist(final long playlistId) { + return playlistStreamTable.getOrderedStreamsOf(playlistId).subscribeOn(Schedulers.io()); + } + + public Single deletePlaylist(final long playlistId) { + return Single.fromCallable(() -> playlistTable.deletePlaylist(playlistId)) .subscribeOn(Schedulers.io()); } } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/MostPlayedFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/local/MostPlayedFragment.java new file mode 100644 index 000000000..466b1d569 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/MostPlayedFragment.java @@ -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 processResult(List results) { + Collections.sort(results, (left, right) -> + ((Long) right.watchCount).compareTo(left.watchCount)); + + List 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); + } +} diff --git a/app/src/main/java/org/schabi/newpipe/fragments/playlist/PlaylistAppendDialog.java b/app/src/main/java/org/schabi/newpipe/fragments/local/PlaylistAppendDialog.java similarity index 98% rename from app/src/main/java/org/schabi/newpipe/fragments/playlist/PlaylistAppendDialog.java rename to app/src/main/java/org/schabi/newpipe/fragments/local/PlaylistAppendDialog.java index bee3b347e..6fad839f1 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/playlist/PlaylistAppendDialog.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/PlaylistAppendDialog.java @@ -1,4 +1,4 @@ -package org.schabi.newpipe.fragments.playlist; +package org.schabi.newpipe.fragments.local; import android.content.Context; import android.os.Bundle; @@ -113,6 +113,7 @@ public class PlaylistAppendDialog extends DialogFragment { .subscribe(metadataEntries -> { if (metadataEntries.isEmpty()) { openCreatePlaylistDialog(); + return; } List playlistInfoItems = new ArrayList<>(metadataEntries.size()); @@ -123,8 +124,6 @@ public class PlaylistAppendDialog extends DialogFragment { playlistAdapter.clearStreamItemList(); playlistAdapter.addInfoItemList(playlistInfoItems); playlistRecyclerView.setVisibility(View.VISIBLE); - - getDialog().setCanceledOnTouchOutside(true); }); } @@ -141,7 +140,7 @@ public class PlaylistAppendDialog extends DialogFragment { public void openCreatePlaylistDialog() { if (streamInfo == null || getFragmentManager() == null) return; - getDialog().dismiss(); PlaylistCreationDialog.newInstance(streamInfo).show(getFragmentManager(), TAG); + getDialog().dismiss(); } } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/playlist/PlaylistCreationDialog.java b/app/src/main/java/org/schabi/newpipe/fragments/local/PlaylistCreationDialog.java similarity index 98% rename from app/src/main/java/org/schabi/newpipe/fragments/playlist/PlaylistCreationDialog.java rename to app/src/main/java/org/schabi/newpipe/fragments/local/PlaylistCreationDialog.java index 15e787e2a..843b84de6 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/playlist/PlaylistCreationDialog.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/PlaylistCreationDialog.java @@ -1,4 +1,4 @@ -package org.schabi.newpipe.fragments.playlist; +package org.schabi.newpipe.fragments.local; import android.app.AlertDialog; import android.app.Dialog; diff --git a/app/src/main/java/org/schabi/newpipe/fragments/playlist/StreamRecordManager.java b/app/src/main/java/org/schabi/newpipe/fragments/local/StreamRecordManager.java similarity index 56% rename from app/src/main/java/org/schabi/newpipe/fragments/playlist/StreamRecordManager.java rename to app/src/main/java/org/schabi/newpipe/fragments/local/StreamRecordManager.java index 31f6284eb..458ec4da2 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/playlist/StreamRecordManager.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/StreamRecordManager.java @@ -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.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.StreamHistoryEntity; import org.schabi.newpipe.extractor.stream.StreamInfo; -import org.schabi.newpipe.extractor.stream.StreamInfoItem; import java.util.Date; import java.util.List; -import io.reactivex.MaybeObserver; +import io.reactivex.Flowable; import io.reactivex.Single; -import io.reactivex.disposables.Disposable; import io.reactivex.schedulers.Schedulers; public class StreamRecordManager { @@ -29,11 +27,6 @@ public class StreamRecordManager { historyTable = db.streamHistoryDAO(); } - public int onChanged(final StreamInfoItem infoItem) { - // Only existing streams are updated - return streamTable.update(new StreamEntity(infoItem)); - } - public Single onViewed(final StreamInfo info) { return Single.fromCallable(() -> database.runInTransaction(() -> { final long streamId = streamTable.upsert(new StreamEntity(info)); @@ -45,30 +38,7 @@ public class StreamRecordManager { return historyTable.deleteHistory(streamId); } - public void removeRecord() { - historyTable.getStatistics().firstElement().subscribe( - new MaybeObserver>() { - - @Override - public void onSubscribe(Disposable d) { - - } - - @Override - public void onSuccess(List streamStatisticsEntries) { - hashCode(); - } - - @Override - public void onError(Throwable e) { - - } - - @Override - public void onComplete() { - - } - } - ); + public Flowable> getStatistics() { + return historyTable.getStatistics(); } } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/WatchHistoryFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/local/WatchHistoryFragment.java new file mode 100644 index 000000000..794872954 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/WatchHistoryFragment.java @@ -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 processResult(List results) { + Collections.sort(results, (left, right) -> + right.latestAccessDate.compareTo(left.latestAccessDate)); + + List 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()); + } +} diff --git a/app/src/main/java/org/schabi/newpipe/info_list/holder/PlaylistMiniInfoItemHolder.java b/app/src/main/java/org/schabi/newpipe/info_list/holder/PlaylistMiniInfoItemHolder.java index 2e8919575..50b551c61 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/holder/PlaylistMiniInfoItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/holder/PlaylistMiniInfoItemHolder.java @@ -47,6 +47,14 @@ public class PlaylistMiniInfoItemHolder extends InfoItemHolder { itemBuilder.getOnPlaylistSelectedListener().selected(item); } }); + + itemView.setLongClickable(true); + itemView.setOnLongClickListener(view -> { + if (itemBuilder.getOnPlaylistSelectedListener() != null) { + itemBuilder.getOnPlaylistSelectedListener().held(item); + } + return true; + }); } /** diff --git a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java index ca863fc8a..3cf169ecd 100644 --- a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java @@ -64,7 +64,7 @@ import com.nostra13.universalimageloader.core.listener.SimpleImageLoadingListene import org.schabi.newpipe.NewPipeDatabase; import org.schabi.newpipe.R; 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.CacheFactory; 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()); - recordManager.removeRecord(); initThumbnail(info == null ? item.getThumbnailUrl() : info.thumbnail_url); } diff --git a/app/src/main/java/org/schabi/newpipe/playlist/SinglePlayQueue.java b/app/src/main/java/org/schabi/newpipe/playlist/SinglePlayQueue.java index ae74528eb..9c4d2fb39 100644 --- a/app/src/main/java/org/schabi/newpipe/playlist/SinglePlayQueue.java +++ b/app/src/main/java/org/schabi/newpipe/playlist/SinglePlayQueue.java @@ -3,19 +3,29 @@ package org.schabi.newpipe.playlist; import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.StreamInfoItem; +import java.util.ArrayList; import java.util.Collections; +import java.util.List; public final class SinglePlayQueue extends PlayQueue { public SinglePlayQueue(final StreamInfoItem item) { - this(new PlayQueueItem(item)); + super(0, Collections.singletonList(new PlayQueueItem(item))); } public SinglePlayQueue(final StreamInfo info) { - this(new PlayQueueItem(info)); + super(0, Collections.singletonList(new PlayQueueItem(info))); } - private SinglePlayQueue(final PlayQueueItem playQueueItem) { - super(0, Collections.singletonList(playQueueItem)); + public SinglePlayQueue(final List items, final int index) { + super(index, playQueueItemsOf(items)); + } + + private static List playQueueItemsOf(List items) { + List playQueueItems = new ArrayList<>(items.size()); + for (final StreamInfoItem item : items) { + playQueueItems.add(new PlayQueueItem(item)); + } + return playQueueItems; } @Override diff --git a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java index 8894af9df..7ffbf07ed 100644 --- a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java @@ -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.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.history.HistoryActivity; import org.schabi.newpipe.player.BackgroundPlayer; import org.schabi.newpipe.player.BackgroundPlayerActivity; @@ -323,6 +326,30 @@ public class NavigationHelper { .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 //////////////////////////////////////////////////////////////////////////*/ diff --git a/app/src/main/res/layout/bookmark_header.xml b/app/src/main/res/layout/bookmark_header.xml new file mode 100644 index 000000000..b087a5157 --- /dev/null +++ b/app/src/main/res/layout/bookmark_header.xml @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_bookmarks.xml b/app/src/main/res/layout/fragment_bookmarks.xml new file mode 100644 index 000000000..56e13225f --- /dev/null +++ b/app/src/main/res/layout/fragment_bookmarks.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_feed.xml b/app/src/main/res/layout/fragment_feed.xml index 0868d8233..d45060440 100644 --- a/app/src/main/res/layout/fragment_feed.xml +++ b/app/src/main/res/layout/fragment_feed.xml @@ -26,7 +26,7 @@ diff --git a/app/src/main/res/layout/local_playlist_header.xml b/app/src/main/res/layout/local_playlist_header.xml new file mode 100644 index 000000000..0ceee5d9a --- /dev/null +++ b/app/src/main/res/layout/local_playlist_header.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/settings_keys.xml b/app/src/main/res/values/settings_keys.xml index 372b917e0..14216dd88 100644 --- a/app/src/main/res/values/settings_keys.xml +++ b/app/src/main/res/values/settings_keys.xml @@ -119,12 +119,14 @@ subscription_page_key kiosk_page channel_page + bookmark_page @string/blank_page_key @string/kiosk_page_key @string/feed_page_key @string/subscription_page_key @string/channel_page_key + @string/bookmark_page_key main_page_selected_service main_page_selected_channel_name diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c94453570..df5b15c19 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -32,6 +32,7 @@ Main Subscriptions + Bookmarks What\'s New @@ -304,6 +305,8 @@ History cleared Item deleted Do you want to delete this item from search history? + Watch History + Most Played Content of main page @@ -370,5 +373,6 @@ Create New Playlist + Delete Playlist Name