diff --git a/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamHistoryDAO.java b/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamHistoryDAO.java index 19c7b9e90..522c03522 100644 --- a/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamHistoryDAO.java +++ b/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamHistoryDAO.java @@ -48,7 +48,6 @@ public abstract class StreamHistoryDAO implements BasicDAO " COUNT(*) AS " + STREAM_WATCH_COUNT + " FROM " + STREAM_HISTORY_TABLE + " GROUP BY " + JOIN_STREAM_ID + ")" + - " ON " + STREAM_ID + " = " + JOIN_STREAM_ID + - " ORDER BY " + STREAM_ACCESS_DATE + " DESC") + " ON " + STREAM_ID + " = " + JOIN_STREAM_ID) public abstract Flowable> getStatistics(); } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/playlist/LocalPlaylistManager.java b/app/src/main/java/org/schabi/newpipe/fragments/playlist/LocalPlaylistManager.java index db32a392e..911b3c7fd 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/playlist/LocalPlaylistManager.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/playlist/LocalPlaylistManager.java @@ -14,6 +14,8 @@ import java.util.List; import io.reactivex.Completable; import io.reactivex.Maybe; +import io.reactivex.Scheduler; +import io.reactivex.schedulers.Schedulers; public class LocalPlaylistManager { @@ -46,7 +48,7 @@ public class LocalPlaylistManager { } return playlistStreamTable.insertAll(joinEntities); - })); + })).subscribeOn(Schedulers.io()); } public Maybe appendToPlaylist(final long playlistId, final StreamEntity stream) { @@ -57,7 +59,7 @@ public class LocalPlaylistManager { return Maybe.zip(streamIdFuture, joinIndexFuture, (streamId, currentMaxJoinIndex) -> playlistStreamTable.insert(new PlaylistStreamEntity(playlistId, streamId, currentMaxJoinIndex + 1)) - ); + ).subscribeOn(Schedulers.io()); } public Completable updateJoin(final long playlistId, final List streamIds) { @@ -73,6 +75,8 @@ public class LocalPlaylistManager { } public Maybe> getPlaylists() { - return playlistStreamTable.getPlaylistMetadata().firstElement(); + return playlistStreamTable.getPlaylistMetadata() + .firstElement() + .subscribeOn(Schedulers.io()); } } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/playlist/PlaylistAppendDialog.java b/app/src/main/java/org/schabi/newpipe/fragments/playlist/PlaylistAppendDialog.java new file mode 100644 index 000000000..bee3b347e --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/playlist/PlaylistAppendDialog.java @@ -0,0 +1,147 @@ +package org.schabi.newpipe.fragments.playlist; + +import android.content.Context; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v4.app.DialogFragment; +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.schabi.newpipe.NewPipeDatabase; +import org.schabi.newpipe.R; +import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry; +import org.schabi.newpipe.database.stream.model.StreamEntity; +import org.schabi.newpipe.extractor.InfoItem; +import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem; +import org.schabi.newpipe.extractor.stream.StreamInfo; +import org.schabi.newpipe.info_list.InfoItemBuilder; +import org.schabi.newpipe.info_list.InfoListAdapter; +import org.schabi.newpipe.info_list.stored.LocalPlaylistInfoItem; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +import io.reactivex.android.schedulers.AndroidSchedulers; + +public class PlaylistAppendDialog extends DialogFragment { + private static final String TAG = PlaylistAppendDialog.class.getCanonicalName(); + private static final String INFO_KEY = "info_key"; + + private StreamInfo streamInfo; + + private View newPlaylistButton; + private RecyclerView playlistRecyclerView; + private InfoListAdapter playlistAdapter; + + public static PlaylistAppendDialog newInstance(final StreamInfo info) { + PlaylistAppendDialog dialog = new PlaylistAppendDialog(); + dialog.setInfo(info); + return dialog; + } + + private void setInfo(StreamInfo info) { + this.streamInfo = info; + } + + /*////////////////////////////////////////////////////////////////////////// + // LifeCycle + //////////////////////////////////////////////////////////////////////////*/ + + @Override + public void onAttach(Context context) { + super.onAttach(context); + playlistAdapter = new InfoListAdapter(getActivity()); + playlistAdapter.useMiniItemVariants(true); + } + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + if (savedInstanceState != null) { + Serializable serial = savedInstanceState.getSerializable(INFO_KEY); + if (serial instanceof StreamInfo) streamInfo = (StreamInfo) serial; + } + } + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + return inflater.inflate(R.layout.dialog_playlists, container); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + newPlaylistButton = view.findViewById(R.id.newPlaylist); + playlistRecyclerView = view.findViewById(R.id.playlist_list); + playlistRecyclerView.setLayoutManager(new LinearLayoutManager(getContext())); + playlistRecyclerView.setAdapter(playlistAdapter); + + final LocalPlaylistManager playlistManager = + new LocalPlaylistManager(NewPipeDatabase.getInstance(getContext())); + + newPlaylistButton.setOnClickListener(ignored -> openCreatePlaylistDialog()); + + playlistAdapter.setOnPlaylistSelectedListener(new InfoItemBuilder.OnInfoItemSelectedListener() { + @Override + public void selected(PlaylistInfoItem selectedItem) { + if (!(selectedItem instanceof LocalPlaylistInfoItem)) return; + final long playlistId = ((LocalPlaylistInfoItem) selectedItem).getPlaylistId(); + final Toast successToast = + Toast.makeText(getContext(), "Added", Toast.LENGTH_SHORT); + + playlistManager.appendToPlaylist(playlistId, new StreamEntity(streamInfo)) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(ignored -> successToast.show()); + + getDialog().dismiss(); + } + + @Override + public void held(PlaylistInfoItem selectedItem) {} + }); + + playlistManager.getPlaylists() + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(metadataEntries -> { + if (metadataEntries.isEmpty()) { + openCreatePlaylistDialog(); + } + + List playlistInfoItems = new ArrayList<>(metadataEntries.size()); + for (final PlaylistMetadataEntry metadataEntry : metadataEntries) { + playlistInfoItems.add(metadataEntry.toStoredPlaylistInfoItem()); + } + + playlistAdapter.clearStreamItemList(); + playlistAdapter.addInfoItemList(playlistInfoItems); + playlistRecyclerView.setVisibility(View.VISIBLE); + + getDialog().setCanceledOnTouchOutside(true); + }); + } + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + outState.putSerializable(INFO_KEY, streamInfo); + } + + /*////////////////////////////////////////////////////////////////////////// + // Helper + //////////////////////////////////////////////////////////////////////////*/ + + public void openCreatePlaylistDialog() { + if (streamInfo == null || getFragmentManager() == null) return; + + getDialog().dismiss(); + PlaylistCreationDialog.newInstance(streamInfo).show(getFragmentManager(), TAG); + } +} diff --git a/app/src/main/java/org/schabi/newpipe/fragments/playlist/PlaylistCreationDialog.java b/app/src/main/java/org/schabi/newpipe/fragments/playlist/PlaylistCreationDialog.java new file mode 100644 index 000000000..15e787e2a --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/playlist/PlaylistCreationDialog.java @@ -0,0 +1,91 @@ +package org.schabi.newpipe.fragments.playlist; + +import android.app.AlertDialog; +import android.app.Dialog; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v4.app.DialogFragment; +import android.view.View; +import android.widget.EditText; +import android.widget.Toast; + +import org.schabi.newpipe.MainActivity; +import org.schabi.newpipe.NewPipeDatabase; +import org.schabi.newpipe.R; +import org.schabi.newpipe.database.stream.model.StreamEntity; +import org.schabi.newpipe.extractor.stream.StreamInfo; + +import java.util.Collections; +import java.util.List; + +import io.reactivex.android.schedulers.AndroidSchedulers; + +public class PlaylistCreationDialog extends DialogFragment { + private static final String TAG = PlaylistCreationDialog.class.getCanonicalName(); + private static final boolean DEBUG = MainActivity.DEBUG; + + private static final String INFO_KEY = "info_key"; + + private StreamInfo streamInfo; + + public static PlaylistCreationDialog newInstance(final StreamInfo info) { + PlaylistCreationDialog dialog = new PlaylistCreationDialog(); + dialog.setInfo(info); + return dialog; + } + + private void setInfo(final StreamInfo info) { + this.streamInfo = info; + } + + /*////////////////////////////////////////////////////////////////////////// + // LifeCycle + //////////////////////////////////////////////////////////////////////////*/ + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + if (streamInfo != null) { + outState.putSerializable(INFO_KEY, streamInfo); + } + } + + @NonNull + @Override + public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { + if (savedInstanceState != null && streamInfo == null) { + final Object infoCandidate = savedInstanceState.getSerializable(INFO_KEY); + if (infoCandidate != null && infoCandidate instanceof StreamInfo) { + streamInfo = (StreamInfo) infoCandidate; + } + } + + if (streamInfo == null) return super.onCreateDialog(savedInstanceState); + + View dialogView = View.inflate(getContext(), + R.layout.dialog_create_playlist, null); + EditText nameInput = dialogView.findViewById(R.id.playlist_name); + + final AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(getContext()) + .setTitle(R.string.create_playlist) + .setView(dialogView) + .setCancelable(true) + .setNegativeButton(R.string.cancel, null) + .setPositiveButton(R.string.create, (dialogInterface, i) -> { + final String name = nameInput.getText().toString(); + final LocalPlaylistManager playlistManager = + new LocalPlaylistManager(NewPipeDatabase.getInstance(getContext())); + final List streams = + Collections.singletonList(new StreamEntity(streamInfo)); + + playlistManager.createPlaylist(name, streams) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(longs -> Toast.makeText(getActivity(), + "Playlist " + name + " successfully created", + Toast.LENGTH_SHORT).show()); + }); + + return dialogBuilder.create(); + } +} diff --git a/app/src/main/java/org/schabi/newpipe/fragments/playlist/StreamRecordManager.java b/app/src/main/java/org/schabi/newpipe/fragments/playlist/StreamRecordManager.java index bd5bd36a2..31f6284eb 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/playlist/StreamRecordManager.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/playlist/StreamRecordManager.java @@ -44,4 +44,31 @@ public class StreamRecordManager { public int removeHistory(final long streamId) { 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() { + + } + } + ); + } } diff --git a/app/src/main/java/org/schabi/newpipe/info_list/InfoItemBuilder.java b/app/src/main/java/org/schabi/newpipe/info_list/InfoItemBuilder.java index ab3d73149..c81235623 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/InfoItemBuilder.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/InfoItemBuilder.java @@ -16,6 +16,7 @@ import org.schabi.newpipe.info_list.holder.ChannelInfoItemHolder; import org.schabi.newpipe.info_list.holder.ChannelMiniInfoItemHolder; import org.schabi.newpipe.info_list.holder.InfoItemHolder; import org.schabi.newpipe.info_list.holder.PlaylistInfoItemHolder; +import org.schabi.newpipe.info_list.holder.PlaylistMiniInfoItemHolder; import org.schabi.newpipe.info_list.holder.StreamInfoItemHolder; import org.schabi.newpipe.info_list.holder.StreamMiniInfoItemHolder; @@ -75,7 +76,7 @@ public class InfoItemBuilder { case CHANNEL: return useMiniVariant ? new ChannelMiniInfoItemHolder(this, parent) : new ChannelInfoItemHolder(this, parent); case PLAYLIST: - return new PlaylistInfoItemHolder(this, parent); + return useMiniVariant ? new PlaylistMiniInfoItemHolder(this, parent) : new PlaylistInfoItemHolder(this, parent); default: Log.e(TAG, "Trollolo"); throw new RuntimeException("InfoType not expected = " + infoType.name()); diff --git a/app/src/main/java/org/schabi/newpipe/info_list/InfoItemDialog.java b/app/src/main/java/org/schabi/newpipe/info_list/InfoItemDialog.java index cdb2191e5..88aa76887 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/InfoItemDialog.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/InfoItemDialog.java @@ -19,7 +19,7 @@ public class InfoItemDialog { @NonNull final StreamInfoItem info, @NonNull final String[] commands, @NonNull final DialogInterface.OnClickListener actions) { - this(activity, commands, actions, info.getName(), info.uploader_name); + this(activity, commands, actions, info.getName(), info.getUploaderName()); } public InfoItemDialog(@NonNull final Activity activity, @@ -28,8 +28,7 @@ public class InfoItemDialog { @NonNull final String title, @Nullable final String additionalDetail) { - final LayoutInflater inflater = activity.getLayoutInflater(); - final View bannerView = inflater.inflate(R.layout.dialog_title, null); + final View bannerView = View.inflate(activity, R.layout.dialog_title, null); bannerView.setSelected(true); TextView titleView = bannerView.findViewById(R.id.itemTitleView); diff --git a/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java b/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java index 806b348d7..5494eae23 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java @@ -15,6 +15,7 @@ import org.schabi.newpipe.info_list.holder.ChannelInfoItemHolder; import org.schabi.newpipe.info_list.holder.ChannelMiniInfoItemHolder; import org.schabi.newpipe.info_list.holder.InfoItemHolder; import org.schabi.newpipe.info_list.holder.PlaylistInfoItemHolder; +import org.schabi.newpipe.info_list.holder.PlaylistMiniInfoItemHolder; import org.schabi.newpipe.info_list.holder.StreamInfoItemHolder; import org.schabi.newpipe.info_list.holder.StreamMiniInfoItemHolder; @@ -52,6 +53,7 @@ public class InfoListAdapter extends RecyclerView.Adapter { + if (itemBuilder.getOnPlaylistSelectedListener() != null) { + itemBuilder.getOnPlaylistSelectedListener().selected(item); + } + }); + } + + /** + * Display options for playlist thumbnails + */ + public static final DisplayImageOptions DISPLAY_THUMBNAIL_OPTIONS = + new DisplayImageOptions.Builder() + .cloneFrom(BASE_DISPLAY_IMAGE_OPTIONS) + .showImageOnLoading(R.drawable.dummy_thumbnail_playlist) + .showImageForEmptyUri(R.drawable.dummy_thumbnail_playlist) + .showImageOnFail(R.drawable.dummy_thumbnail_playlist) + .build(); +} diff --git a/app/src/main/java/org/schabi/newpipe/info_list/stored/LocalPlaylistInfoItem.java b/app/src/main/java/org/schabi/newpipe/info_list/stored/LocalPlaylistInfoItem.java index 63f61cc43..3ac5fabb7 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/stored/LocalPlaylistInfoItem.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/stored/LocalPlaylistInfoItem.java @@ -1,30 +1,20 @@ package org.schabi.newpipe.info_list.stored; -import org.schabi.newpipe.extractor.InfoItem; +import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem; import static org.schabi.newpipe.util.Constants.NO_SERVICE_ID; import static org.schabi.newpipe.util.Constants.NO_URL; -public class LocalPlaylistInfoItem extends InfoItem { +public class LocalPlaylistInfoItem extends PlaylistInfoItem { private final long playlistId; - private long streamCount; public LocalPlaylistInfoItem(final long playlistId, final String name) { - super(InfoType.PLAYLIST, NO_SERVICE_ID, NO_URL, name); + super(NO_SERVICE_ID, NO_URL, name); this.playlistId = playlistId; - this.streamCount = streamCount; } public long getPlaylistId() { return playlistId; } - - public long getStreamCount() { - return streamCount; - } - - public void setStreamCount(long streamCount) { - this.streamCount = streamCount; - } } diff --git a/app/src/main/java/org/schabi/newpipe/info_list/stored/StreamStatisticsInfoItem.java b/app/src/main/java/org/schabi/newpipe/info_list/stored/StreamStatisticsInfoItem.java index ef82826ba..76984d363 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/stored/StreamStatisticsInfoItem.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/stored/StreamStatisticsInfoItem.java @@ -1,18 +1,19 @@ package org.schabi.newpipe.info_list.stored; -import org.schabi.newpipe.extractor.InfoItem; +import org.schabi.newpipe.extractor.stream.StreamInfoItem; +import org.schabi.newpipe.extractor.stream.StreamType; import java.util.Date; -public class StreamStatisticsInfoItem extends InfoItem { +public class StreamStatisticsInfoItem extends StreamInfoItem { private final long streamId; private Date latestAccessDate; private long watchCount; public StreamStatisticsInfoItem(final long streamId, final int serviceId, - final String url, final String name) { - super(InfoType.STREAM, serviceId, url, name); + final String url, final String name, final StreamType type) { + super(serviceId, url, name, type); this.streamId = streamId; } 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 ad2200bfc..ca863fc8a 100644 --- a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java @@ -81,7 +81,6 @@ import io.reactivex.Observable; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.CompositeDisposable; import io.reactivex.disposables.Disposable; -import io.reactivex.schedulers.Schedulers; import static org.schabi.newpipe.player.helper.PlayerHelper.getTimeString; diff --git a/app/src/main/res/layout/dialog_create_playlist.xml b/app/src/main/res/layout/dialog_create_playlist.xml new file mode 100644 index 000000000..b42d3101f --- /dev/null +++ b/app/src/main/res/layout/dialog_create_playlist.xml @@ -0,0 +1,22 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_playlists.xml b/app/src/main/res/layout/dialog_playlists.xml new file mode 100644 index 000000000..5abe91a8e --- /dev/null +++ b/app/src/main/res/layout/dialog_playlists.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/layout/list_playlist_mini_item.xml b/app/src/main/res/layout/list_playlist_mini_item.xml new file mode 100644 index 000000000..3e854bb8e --- /dev/null +++ b/app/src/main/res/layout/list_playlist_mini_item.xml @@ -0,0 +1,70 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5d05d088d..c94453570 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -224,6 +224,7 @@ Start Pause Play + Create Delete Checksum @@ -353,6 +354,7 @@ YouTube SoundCloud + @string/preferred_player_settings_title Open with preferred player @@ -365,4 +367,8 @@ Getting info… "The requested content is loading" + + + Create New Playlist + Name