diff --git a/app/src/main/java/org/schabi/newpipe/database/LocalItem.java b/app/src/main/java/org/schabi/newpipe/database/LocalItem.java new file mode 100644 index 00000000000..95d0d921345 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/LocalItem.java @@ -0,0 +1,11 @@ +package org.schabi.newpipe.database; + +public interface LocalItem { + enum LocalItemType { + PLAYLIST_ITEM, + PLAYLIST_STREAM_ITEM, + STATISTIC_STREAM_ITEM + } + + LocalItemType getLocalItemType(); +} diff --git a/app/src/main/java/org/schabi/newpipe/database/history/dao/WatchHistoryDAO.java b/app/src/main/java/org/schabi/newpipe/database/history/dao/WatchHistoryDAO.java deleted file mode 100644 index a01d8e46dc9..00000000000 --- a/app/src/main/java/org/schabi/newpipe/database/history/dao/WatchHistoryDAO.java +++ /dev/null @@ -1,37 +0,0 @@ -package org.schabi.newpipe.database.history.dao; - -import android.arch.persistence.room.Dao; -import android.arch.persistence.room.Query; - -import org.schabi.newpipe.database.history.model.WatchHistoryEntry; - -import java.util.List; - -import io.reactivex.Flowable; - -import static org.schabi.newpipe.database.history.model.WatchHistoryEntry.CREATION_DATE; -import static org.schabi.newpipe.database.history.model.WatchHistoryEntry.ID; -import static org.schabi.newpipe.database.history.model.WatchHistoryEntry.SERVICE_ID; -import static org.schabi.newpipe.database.history.model.WatchHistoryEntry.TABLE_NAME; - -@Dao -public interface WatchHistoryDAO extends HistoryDAO { - - String ORDER_BY_CREATION_DATE = " ORDER BY " + CREATION_DATE + " DESC"; - - @Query("SELECT * FROM " + TABLE_NAME + " WHERE " + ID + " = (SELECT MAX(" + ID + ") FROM " + TABLE_NAME + ")") - @Override - WatchHistoryEntry getLatestEntry(); - - @Query("DELETE FROM " + TABLE_NAME) - @Override - int deleteAll(); - - @Query("SELECT * FROM " + TABLE_NAME + ORDER_BY_CREATION_DATE) - @Override - Flowable> getAll(); - - @Query("SELECT * FROM " + TABLE_NAME + " WHERE " + SERVICE_ID + " = :serviceId" + ORDER_BY_CREATION_DATE) - @Override - Flowable> listByService(int serviceId); -} diff --git a/app/src/main/java/org/schabi/newpipe/database/history/model/WatchHistoryEntry.java b/app/src/main/java/org/schabi/newpipe/database/history/model/WatchHistoryEntry.java deleted file mode 100644 index bfd84d37769..00000000000 --- a/app/src/main/java/org/schabi/newpipe/database/history/model/WatchHistoryEntry.java +++ /dev/null @@ -1,109 +0,0 @@ -package org.schabi.newpipe.database.history.model; - -import android.arch.persistence.room.ColumnInfo; -import android.arch.persistence.room.Entity; -import android.arch.persistence.room.Ignore; - -import org.schabi.newpipe.extractor.stream.StreamInfo; - -import java.util.Date; - -@Entity(tableName = WatchHistoryEntry.TABLE_NAME) -public class WatchHistoryEntry extends HistoryEntry { - - public static final String TABLE_NAME = "watch_history"; - public static final String TITLE = "title"; - public static final String URL = "url"; - public static final String STREAM_ID = "stream_id"; - public static final String THUMBNAIL_URL = "thumbnail_url"; - public static final String UPLOADER = "uploader"; - public static final String DURATION = "duration"; - - @ColumnInfo(name = TITLE) - private String title; - - @ColumnInfo(name = URL) - private String url; - - @ColumnInfo(name = STREAM_ID) - private String streamId; - - @ColumnInfo(name = THUMBNAIL_URL) - private String thumbnailURL; - - @ColumnInfo(name = UPLOADER) - private String uploader; - - @ColumnInfo(name = DURATION) - private long duration; - - public WatchHistoryEntry(Date creationDate, int serviceId, String title, String url, String streamId, String thumbnailURL, String uploader, long duration) { - super(creationDate, serviceId); - this.title = title; - this.url = url; - this.streamId = streamId; - this.thumbnailURL = thumbnailURL; - this.uploader = uploader; - this.duration = duration; - } - - public WatchHistoryEntry(StreamInfo streamInfo) { - this(new Date(), streamInfo.getServiceId(), streamInfo.getName(), streamInfo.getUrl(), - streamInfo.id, streamInfo.thumbnail_url, streamInfo.uploader_name, streamInfo.duration); - } - - public String getUrl() { - return url; - } - - public void setUrl(String url) { - this.url = url; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public String getStreamId() { - return streamId; - } - - public void setStreamId(String streamId) { - this.streamId = streamId; - } - - public String getThumbnailURL() { - return thumbnailURL; - } - - public void setThumbnailURL(String thumbnailURL) { - this.thumbnailURL = thumbnailURL; - } - - public String getUploader() { - return uploader; - } - - public void setUploader(String uploader) { - this.uploader = uploader; - } - - public long getDuration() { - return duration; - } - - public void setDuration(int duration) { - this.duration = duration; - } - - @Ignore - @Override - public boolean hasEqualValues(HistoryEntry otherEntry) { - return otherEntry instanceof WatchHistoryEntry && super.hasEqualValues(otherEntry) - && getUrl().equals(((WatchHistoryEntry) otherEntry).getUrl()); - } -} diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistMetadataEntry.java b/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistMetadataEntry.java index 53ae3d48a0f..2daea298bf4 100644 --- a/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistMetadataEntry.java +++ b/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistMetadataEntry.java @@ -2,13 +2,14 @@ import android.arch.persistence.room.ColumnInfo; +import org.schabi.newpipe.database.LocalItem; import org.schabi.newpipe.info_list.stored.LocalPlaylistInfoItem; import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_ID; import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_NAME; import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_THUMBNAIL_URL; -public class PlaylistMetadataEntry { +public class PlaylistMetadataEntry implements LocalItem { final public static String PLAYLIST_STREAM_COUNT = "streamCount"; @ColumnInfo(name = PLAYLIST_ID) @@ -33,4 +34,9 @@ public LocalPlaylistInfoItem toStoredPlaylistInfoItem() { storedPlaylistInfoItem.setStreamCount(streamCount); return storedPlaylistInfoItem; } + + @Override + public LocalItemType getLocalItemType() { + return LocalItemType.PLAYLIST_ITEM; + } } diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistStreamEntry.java b/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistStreamEntry.java new file mode 100644 index 00000000000..b6ecfe1f017 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistStreamEntry.java @@ -0,0 +1,60 @@ +package org.schabi.newpipe.database.playlist; + +import android.arch.persistence.room.ColumnInfo; + +import org.schabi.newpipe.database.LocalItem; +import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity; +import org.schabi.newpipe.database.stream.model.StreamEntity; +import org.schabi.newpipe.extractor.stream.StreamInfoItem; +import org.schabi.newpipe.extractor.stream.StreamType; + +public class PlaylistStreamEntry implements LocalItem { + @ColumnInfo(name = StreamEntity.STREAM_ID) + final public long uid; + @ColumnInfo(name = StreamEntity.STREAM_SERVICE_ID) + final public int serviceId; + @ColumnInfo(name = StreamEntity.STREAM_URL) + final public String url; + @ColumnInfo(name = StreamEntity.STREAM_TITLE) + final public String title; + @ColumnInfo(name = StreamEntity.STREAM_TYPE) + final public StreamType streamType; + @ColumnInfo(name = StreamEntity.STREAM_DURATION) + final public long duration; + @ColumnInfo(name = StreamEntity.STREAM_UPLOADER) + final public String uploader; + @ColumnInfo(name = StreamEntity.STREAM_THUMBNAIL_URL) + final public String thumbnailUrl; + @ColumnInfo(name = PlaylistStreamEntity.JOIN_STREAM_ID) + final public long streamId; + @ColumnInfo(name = PlaylistStreamEntity.JOIN_INDEX) + final public int joinIndex; + + public PlaylistStreamEntry(long uid, int serviceId, String url, String title, + StreamType streamType, long duration, String uploader, + String thumbnailUrl, long streamId, int joinIndex) { + this.uid = uid; + this.serviceId = serviceId; + this.url = url; + this.title = title; + this.streamType = streamType; + this.duration = duration; + this.uploader = uploader; + this.thumbnailUrl = thumbnailUrl; + this.streamId = streamId; + this.joinIndex = joinIndex; + } + + public StreamInfoItem toStreamInfoItem() throws IllegalArgumentException { + StreamInfoItem item = new StreamInfoItem(serviceId, url, title, streamType); + item.setThumbnailUrl(thumbnailUrl); + item.setUploaderName(uploader); + item.setDuration(duration); + return item; + } + + @Override + public LocalItemType getLocalItemType() { + return LocalItemType.PLAYLIST_STREAM_ITEM; + } +} diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistStreamDAO.java b/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistStreamDAO.java index b9f325aa229..2d645e793f4 100644 --- a/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistStreamDAO.java +++ b/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistStreamDAO.java @@ -6,6 +6,7 @@ import org.schabi.newpipe.database.BasicDAO; import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry; +import org.schabi.newpipe.database.playlist.PlaylistStreamEntry; import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity; import org.schabi.newpipe.database.stream.model.StreamEntity; @@ -37,17 +38,13 @@ public Flowable> listByService(int serviceId) { " WHERE " + JOIN_PLAYLIST_ID + " = :playlistId") public abstract void deleteBatch(final long playlistId); - @Query("SELECT MAX(" + JOIN_INDEX + ")" + + @Query("SELECT COALESCE(MAX(" + JOIN_INDEX + "), -1)" + " FROM " + PLAYLIST_STREAM_JOIN_TABLE + " WHERE " + JOIN_PLAYLIST_ID + " = :playlistId") public abstract Flowable getMaximumIndexOf(final long playlistId); @Transaction - @Query("SELECT " + STREAM_ID + ", " + STREAM_SERVICE_ID + ", " + STREAM_URL + ", " + - STREAM_TITLE + ", " + STREAM_TYPE + ", " + STREAM_UPLOADER + ", " + - STREAM_DURATION + ", " + STREAM_THUMBNAIL_URL + - - " FROM " + STREAM_TABLE + " INNER JOIN " + + @Query("SELECT * FROM " + STREAM_TABLE + " INNER JOIN " + // get ids of streams of the given playlist "(SELECT " + JOIN_STREAM_ID + "," + JOIN_INDEX + " FROM " + PLAYLIST_STREAM_JOIN_TABLE + " WHERE " @@ -56,14 +53,16 @@ public Flowable> listByService(int serviceId) { // then merge with the stream metadata " ON " + STREAM_ID + " = " + JOIN_STREAM_ID + " ORDER BY " + JOIN_INDEX + " ASC") - public abstract Flowable> getOrderedStreamsOf(long playlistId); + public abstract Flowable> getOrderedStreamsOf(long playlistId); @Transaction @Query("SELECT " + PLAYLIST_ID + ", " + PLAYLIST_NAME + ", " + - PLAYLIST_THUMBNAIL_URL + ", COUNT(*) AS " + PLAYLIST_STREAM_COUNT + + PLAYLIST_THUMBNAIL_URL + ", " + + "COALESCE(COUNT(" + JOIN_PLAYLIST_ID + "), 0) AS " + PLAYLIST_STREAM_COUNT + - " FROM " + PLAYLIST_TABLE + " LEFT JOIN " + PLAYLIST_STREAM_JOIN_TABLE + - " ON " + PLAYLIST_TABLE + "." + PLAYLIST_ID + " = " + PLAYLIST_STREAM_JOIN_TABLE + "." + JOIN_PLAYLIST_ID + + " FROM " + PLAYLIST_TABLE + + " LEFT JOIN " + PLAYLIST_STREAM_JOIN_TABLE + + " ON " + PLAYLIST_ID + " = " + JOIN_PLAYLIST_ID + " GROUP BY " + JOIN_PLAYLIST_ID) public abstract Flowable> getPlaylistMetadata(); } 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 1c2a7028e2e..6909f33970b 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 @@ -2,14 +2,15 @@ import android.arch.persistence.room.ColumnInfo; +import org.schabi.newpipe.database.LocalItem; import org.schabi.newpipe.database.history.model.StreamHistoryEntity; import org.schabi.newpipe.database.stream.model.StreamEntity; +import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.extractor.stream.StreamType; -import org.schabi.newpipe.info_list.stored.StreamStatisticsInfoItem; import java.util.Date; -public class StreamStatisticsEntry { +public class StreamStatisticsEntry implements LocalItem { final public static String STREAM_LATEST_DATE = "latestAccess"; final public static String STREAM_WATCH_COUNT = "watchCount"; @@ -53,14 +54,16 @@ public StreamStatisticsEntry(long uid, int serviceId, String url, String title, this.watchCount = watchCount; } - public StreamStatisticsInfoItem toStreamStatisticsInfoItem() { - StreamStatisticsInfoItem item = - new StreamStatisticsInfoItem(uid, serviceId, url, title, streamType); + public StreamInfoItem toStreamInfoItem() { + StreamInfoItem item = new StreamInfoItem(serviceId, url, title, streamType); item.setDuration(duration); item.setUploaderName(uploader); item.setThumbnailUrl(thumbnailUrl); - item.setLatestAccessDate(latestAccessDate); - item.setWatchCount(watchCount); return item; } + + @Override + public LocalItemType getLocalItemType() { + return LocalItemType.STATISTIC_STREAM_ITEM; + } } diff --git a/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamEntity.java b/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamEntity.java index eb078a03cd4..2fddaa1bbb2 100644 --- a/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamEntity.java +++ b/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamEntity.java @@ -9,7 +9,6 @@ import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.extractor.stream.StreamType; -import org.schabi.newpipe.info_list.stored.StreamEntityInfoItem; import org.schabi.newpipe.playlist.PlayQueueItem; import org.schabi.newpipe.util.Constants; @@ -88,16 +87,6 @@ public StreamEntity(final PlayQueueItem item) { item.getThumbnailUrl(), item.getUploader(), item.getDuration()); } - @Ignore - public StreamEntityInfoItem toStreamEntityInfoItem() throws IllegalArgumentException { - StreamEntityInfoItem item = new StreamEntityInfoItem(getUid(), getServiceId(), - getUrl(), getTitle(), getStreamType()); - item.setThumbnailUrl(getThumbnailUrl()); - item.setUploaderName(getUploader()); - item.setDuration(getDuration()); - return item; - } - public long getUid() { return uid; } 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 14639156700..f7f9f31ba8d 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/MainFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/MainFragment.java @@ -28,7 +28,7 @@ import org.schabi.newpipe.fragments.list.channel.ChannelFragment; import org.schabi.newpipe.fragments.list.feed.FeedFragment; import org.schabi.newpipe.fragments.list.kiosk.KioskFragment; -import org.schabi.newpipe.fragments.local.BookmarkFragment; +import org.schabi.newpipe.fragments.local.bookmark.BookmarkFragment; import org.schabi.newpipe.fragments.subscription.SubscriptionFragment; import org.schabi.newpipe.report.ErrorActivity; import org.schabi.newpipe.report.UserAction; diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/BaseLocalListFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/local/BaseLocalListFragment.java new file mode 100644 index 00000000000..afc67aa687d --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/BaseLocalListFragment.java @@ -0,0 +1,184 @@ +package org.schabi.newpipe.fragments.local; + +import android.content.Context; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.v7.app.ActionBar; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.util.Log; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.View; + +import org.schabi.newpipe.R; +import org.schabi.newpipe.database.LocalItem; +import org.schabi.newpipe.fragments.BaseStateFragment; +import org.schabi.newpipe.fragments.list.ListViewContract; +import org.schabi.newpipe.util.StateSaver; + +import java.util.List; +import java.util.Queue; + +import static org.schabi.newpipe.util.AnimationUtils.animateView; + +public abstract class BaseLocalListFragment extends BaseStateFragment + implements ListViewContract, StateSaver.WriteRead { + + /*////////////////////////////////////////////////////////////////////////// + // Views + //////////////////////////////////////////////////////////////////////////*/ + + protected LocalItemListAdapter itemListAdapter; + protected RecyclerView itemsList; + + /*////////////////////////////////////////////////////////////////////////// + // LifeCycle + //////////////////////////////////////////////////////////////////////////*/ + + @Override + public void onAttach(Context context) { + super.onAttach(context); + itemListAdapter = new LocalItemListAdapter(activity); + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setHasOptionsMenu(true); + } + + @Override + public void onDestroy() { + super.onDestroy(); + StateSaver.onDestroy(savedState); + } + + /*////////////////////////////////////////////////////////////////////////// + // State Saving + //////////////////////////////////////////////////////////////////////////*/ + + protected StateSaver.SavedState savedState; + + @Override + public String generateSuffix() { + // Naive solution, but it's good for now (the items don't change) + return "." + itemListAdapter.getItemsList().size() + ".list"; + } + + @Override + public void writeTo(Queue objectsToSave) { + objectsToSave.add(itemListAdapter.getItemsList()); + } + + @Override + @SuppressWarnings("unchecked") + public void readFrom(@NonNull Queue savedObjects) throws Exception { + itemListAdapter.getItemsList().clear(); + itemListAdapter.getItemsList().addAll((List) savedObjects.poll()); + } + + @Override + public void onSaveInstanceState(Bundle bundle) { + super.onSaveInstanceState(bundle); + savedState = StateSaver.tryToSave(activity.isChangingConfigurations(), savedState, bundle, this); + } + + @Override + protected void onRestoreInstanceState(@NonNull Bundle bundle) { + super.onRestoreInstanceState(bundle); + savedState = StateSaver.tryToRestore(bundle, this); + } + + /*////////////////////////////////////////////////////////////////////////// + // Init + //////////////////////////////////////////////////////////////////////////*/ + + protected View getListHeader() { + return null; + } + + protected View getListFooter() { + return activity.getLayoutInflater().inflate(R.layout.pignate_footer, itemsList, false); + } + + protected RecyclerView.LayoutManager getListLayoutManager() { + return new LinearLayoutManager(activity); + } + + @Override + protected void initViews(View rootView, Bundle savedInstanceState) { + super.initViews(rootView, savedInstanceState); + + itemsList = rootView.findViewById(R.id.items_list); + itemsList.setLayoutManager(getListLayoutManager()); + + itemListAdapter.setFooter(getListFooter()); + itemListAdapter.setHeader(getListHeader()); + + itemsList.setAdapter(itemListAdapter); + } + + @Override + protected void initListeners() { + super.initListeners(); + } + + /*////////////////////////////////////////////////////////////////////////// + // Menu + //////////////////////////////////////////////////////////////////////////*/ + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + if (DEBUG) Log.d(TAG, "onCreateOptionsMenu() called with: menu = [" + menu + "], inflater = [" + inflater + "]"); + super.onCreateOptionsMenu(menu, inflater); + ActionBar supportActionBar = activity.getSupportActionBar(); + if (supportActionBar != null) { + supportActionBar.setDisplayShowTitleEnabled(true); + if(useAsFrontPage) { + supportActionBar.setDisplayHomeAsUpEnabled(false); + } else { + supportActionBar.setDisplayHomeAsUpEnabled(true); + } + } + } + + /*////////////////////////////////////////////////////////////////////////// + // Contract + //////////////////////////////////////////////////////////////////////////*/ + + @Override + public void showLoading() { + super.showLoading(); + // animateView(itemsList, false, 400); + } + + @Override + public void hideLoading() { + super.hideLoading(); + animateView(itemsList, true, 300); + } + + @Override + public void showError(String message, boolean showRetryButton) { + super.showError(message, showRetryButton); + showListFooter(false); + animateView(itemsList, false, 200); + } + + @Override + public void showEmptyState() { + super.showEmptyState(); + showListFooter(false); + } + + @Override + public void showListFooter(final boolean show) { + itemsList.post(() -> itemListAdapter.showFooter(show)); + } + + @Override + public void handleNextItems(N result) { + isLoading.set(false); + } +} diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/HeaderFooterHolder.java b/app/src/main/java/org/schabi/newpipe/fragments/local/HeaderFooterHolder.java new file mode 100644 index 00000000000..3c0830751e3 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/HeaderFooterHolder.java @@ -0,0 +1,13 @@ +package org.schabi.newpipe.fragments.local; + +import android.support.v7.widget.RecyclerView; +import android.view.View; + +public class HeaderFooterHolder extends RecyclerView.ViewHolder { + public View view; + + public HeaderFooterHolder(View v) { + super(v); + view = v; + } +} \ No newline at end of file diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/LocalItemBuilder.java b/app/src/main/java/org/schabi/newpipe/fragments/local/LocalItemBuilder.java new file mode 100644 index 00000000000..d31c8571204 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/LocalItemBuilder.java @@ -0,0 +1,56 @@ +package org.schabi.newpipe.fragments.local; + +import android.content.Context; + +import com.nostra13.universalimageloader.core.ImageLoader; + +import org.schabi.newpipe.database.LocalItem; + +/* + * Created by Christian Schabesberger on 26.09.16. + *

+ * Copyright (C) Christian Schabesberger 2016 + * InfoItemBuilder.java is part of NewPipe. + *

+ * NewPipe is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + *

+ * NewPipe is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + *

+ * You should have received a copy of the GNU General Public License + * along with NewPipe. If not, see . + */ + +public class LocalItemBuilder { + private static final String TAG = LocalItemBuilder.class.toString(); + + private final Context context; + private ImageLoader imageLoader = ImageLoader.getInstance(); + + private OnCustomItemGesture onSelectedListener; + + public LocalItemBuilder(Context context) { + this.context = context; + } + + public Context getContext() { + return context; + } + + public ImageLoader getImageLoader() { + return imageLoader; + } + + public OnCustomItemGesture getOnItemSelectedListener() { + return onSelectedListener; + } + + public void setOnItemSelectedListener(OnCustomItemGesture listener) { + this.onSelectedListener = listener; + } +} diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/LocalItemListAdapter.java b/app/src/main/java/org/schabi/newpipe/fragments/local/LocalItemListAdapter.java new file mode 100644 index 00000000000..9c621a55cb1 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/LocalItemListAdapter.java @@ -0,0 +1,243 @@ +package org.schabi.newpipe.fragments.local; + +import android.app.Activity; +import android.support.v7.widget.RecyclerView; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; + +import org.schabi.newpipe.database.LocalItem; +import org.schabi.newpipe.fragments.local.holder.LocalItemHolder; +import org.schabi.newpipe.fragments.local.holder.LocalPlaylistItemHolder; +import org.schabi.newpipe.fragments.local.holder.LocalPlaylistStreamItemHolder; +import org.schabi.newpipe.fragments.local.holder.LocalStatisticStreamItemHolder; +import org.schabi.newpipe.util.Localization; + +import java.text.DateFormat; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/* + * Created by Christian Schabesberger on 01.08.16. + * + * Copyright (C) Christian Schabesberger 2016 + * InfoListAdapter.java is part of NewPipe. + * + * NewPipe is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * NewPipe is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with NewPipe. If not, see . + */ + +public class LocalItemListAdapter extends RecyclerView.Adapter { + + private static final String TAG = LocalItemListAdapter.class.getSimpleName(); + private static final boolean DEBUG = false; + + private static final int HEADER_TYPE = 0; + private static final int FOOTER_TYPE = 1; + + private static final int STREAM_STATISTICS_HOLDER_TYPE = 0x1000; + private static final int STREAM_PLAYLIST_HOLDER_TYPE = 0x1001; + private static final int PLAYLIST_HOLDER_TYPE = 0x2000; + + private final LocalItemBuilder localItemBuilder; + private final ArrayList localItems; + private final DateFormat dateFormat; + + private boolean showFooter = false; + private View header = null; + private View footer = null; + + + public LocalItemListAdapter(Activity activity) { + localItemBuilder = new LocalItemBuilder(activity); + localItems = new ArrayList<>(); + dateFormat = DateFormat.getDateInstance(DateFormat.SHORT, + Localization.getPreferredLocale(activity)); + } + + public void setSelectedListener(OnCustomItemGesture listener) { + localItemBuilder.setOnItemSelectedListener(listener); + } + + public void addInfoItemList(List data) { + if (data != null) { + if (DEBUG) { + Log.d(TAG, "addInfoItemList() before > localItems.size() = " + + localItems.size() + ", data.size() = " + data.size()); + } + + int offsetStart = sizeConsideringHeader(); + localItems.addAll(data); + + if (DEBUG) { + Log.d(TAG, "addInfoItemList() after > offsetStart = " + offsetStart + + ", localItems.size() = " + localItems.size() + + ", header = " + header + ", footer = " + footer + + ", showFooter = " + showFooter); + } + + notifyItemRangeInserted(offsetStart, data.size()); + + if (footer != null && showFooter) { + int footerNow = sizeConsideringHeader(); + notifyItemMoved(offsetStart, footerNow); + + if (DEBUG) Log.d(TAG, "addInfoItemList() footer from " + offsetStart + + " to " + footerNow); + } + } + } + + public void addInfoItem(LocalItem data) { + addInfoItemList(Collections.singletonList(data)); + } + + public void removeItemAt(final int infoListPosition) { + if (infoListPosition < 0 || infoListPosition >= localItems.size()) return; + + localItems.remove(infoListPosition); + + notifyItemRemoved(infoListPosition + (header != null ? 1 : 0)); + } + + public boolean swapItems(int fromAdapterPosition, int toAdapterPosition) { + final int actualFrom = offsetWithoutHeader(fromAdapterPosition); + final int actualTo = offsetWithoutHeader(toAdapterPosition); + + if (actualFrom < 0 || actualTo < 0) return false; + if (actualFrom >= localItems.size() || actualTo >= localItems.size()) return false; + + localItems.add(actualTo, localItems.remove(actualFrom)); + notifyItemMoved(fromAdapterPosition, toAdapterPosition); + return true; + } + + public void clearStreamItemList() { + if (localItems.isEmpty()) { + return; + } + localItems.clear(); + notifyDataSetChanged(); + } + + public void setHeader(View header) { + boolean changed = header != this.header; + this.header = header; + if (changed) notifyDataSetChanged(); + } + + public void setFooter(View view) { + this.footer = view; + } + + public void showFooter(boolean show) { + if (DEBUG) Log.d(TAG, "showFooter() called with: show = [" + show + "]"); + if (show == showFooter) return; + + showFooter = show; + if (show) notifyItemInserted(sizeConsideringHeader()); + else notifyItemRemoved(sizeConsideringHeader()); + } + + private int offsetWithoutHeader(final int offset) { + return offset - (header != null ? 1 : 0); + } + + private int sizeConsideringHeader() { + return localItems.size() + (header != null ? 1 : 0); + } + + public ArrayList getItemsList() { + return localItems; + } + + @Override + public int getItemCount() { + int count = localItems.size(); + if (header != null) count++; + if (footer != null && showFooter) count++; + + if (DEBUG) { + Log.d(TAG, "getItemCount() called, count = " + count + + ", localItems.size() = " + localItems.size() + + ", header = " + header + ", footer = " + footer + + ", showFooter = " + showFooter); + } + return count; + } + + @Override + public int getItemViewType(int position) { + if (DEBUG) Log.d(TAG, "getItemViewType() called with: position = [" + position + "]"); + + if (header != null && position == 0) { + return HEADER_TYPE; + } else if (header != null) { + position--; + } + if (footer != null && position == localItems.size() && showFooter) { + return FOOTER_TYPE; + } + final LocalItem item = localItems.get(position); + + switch (item.getLocalItemType()) { + case PLAYLIST_ITEM: return PLAYLIST_HOLDER_TYPE; + case PLAYLIST_STREAM_ITEM: return STREAM_PLAYLIST_HOLDER_TYPE; + case STATISTIC_STREAM_ITEM: return STREAM_STATISTICS_HOLDER_TYPE; + default: + Log.e(TAG, "No holder type has been considered for item: [" + + item.getLocalItemType() + "]"); + return -1; + } + } + + @Override + public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int type) { + if (DEBUG) Log.d(TAG, "onCreateViewHolder() called with: parent = [" + + parent + "], type = [" + type + "]"); + switch (type) { + case HEADER_TYPE: + return new HeaderFooterHolder(header); + case FOOTER_TYPE: + return new HeaderFooterHolder(footer); + case PLAYLIST_HOLDER_TYPE: + return new LocalPlaylistItemHolder(localItemBuilder, parent); + case STREAM_PLAYLIST_HOLDER_TYPE: + return new LocalPlaylistStreamItemHolder(localItemBuilder, parent); + case STREAM_STATISTICS_HOLDER_TYPE: + return new LocalStatisticStreamItemHolder(localItemBuilder, parent); + default: + Log.e(TAG, "No view type has been considered for holder: [" + type + "]"); + return null; + } + } + + @Override + public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { + if (DEBUG) Log.d(TAG, "onBindViewHolder() called with: holder = [" + + holder.getClass().getSimpleName() + "], position = [" + position + "]"); + + if (holder instanceof LocalItemHolder) { + // If header isn't null, offset the items by -1 + if (header != null) position--; + + ((LocalItemHolder) holder).updateFromItem(localItems.get(position), dateFormat); + } else if (holder instanceof HeaderFooterHolder && position == 0 && header != null) { + ((HeaderFooterHolder) holder).view = header; + } else if (holder instanceof HeaderFooterHolder && position == sizeConsideringHeader() + && footer != null && showFooter) { + ((HeaderFooterHolder) holder).view = footer; + } + } +} 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 index 7ba5db7e111..2c4af25d90b 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistFragment.java @@ -7,25 +7,29 @@ import android.os.Parcelable; import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import android.support.v7.app.AlertDialog; +import android.support.v7.widget.RecyclerView; +import android.support.v7.widget.helper.ItemTouchHelper; import android.text.TextUtils; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.widget.EditText; import android.widget.TextView; +import android.widget.Toast; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; import org.schabi.newpipe.NewPipeDatabase; import org.schabi.newpipe.R; -import org.schabi.newpipe.database.stream.model.StreamEntity; -import org.schabi.newpipe.extractor.InfoItem; +import org.schabi.newpipe.database.LocalItem; +import org.schabi.newpipe.database.playlist.PlaylistStreamEntry; import org.schabi.newpipe.extractor.stream.StreamInfoItem; -import org.schabi.newpipe.fragments.list.BaseListFragment; import org.schabi.newpipe.info_list.InfoItemDialog; -import org.schabi.newpipe.info_list.OnInfoItemGesture; import org.schabi.newpipe.playlist.PlayQueue; import org.schabi.newpipe.playlist.SinglePlayQueue; import org.schabi.newpipe.report.UserAction; +import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.NavigationHelper; import java.util.ArrayList; @@ -37,7 +41,7 @@ import static org.schabi.newpipe.util.AnimationUtils.animateView; -public class LocalPlaylistFragment extends BaseListFragment, Void> { +public class LocalPlaylistFragment extends BaseLocalListFragment, Void> { private View headerRootLayout; private TextView headerTitleView; @@ -49,12 +53,14 @@ public class LocalPlaylistFragment extends BaseListFragment, private View headerBackgroundButton; @State - protected long playlistId; + protected Long playlistId; @State protected String name; @State protected Parcelable itemsListState; + private ItemTouchHelper itemTouchHelper; + /* Used for independent events */ private CompositeDisposable disposables = new CompositeDisposable(); private Subscription databaseSubscription; @@ -86,6 +92,9 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Override public void onPause() { super.onPause(); + + saveJoin(); + itemsListState = itemsList.getLayoutManager().onSaveInstanceState(); } @@ -115,8 +124,6 @@ public void onDestroy() { @Override protected void initViews(View rootView, Bundle savedInstanceState) { super.initViews(rootView, savedInstanceState); - infoListAdapter.useMiniItemVariants(true); - setFragmentTitle(name); } @@ -141,44 +148,61 @@ protected View getListHeader() { protected void initListeners() { super.initListeners(); - infoListAdapter.setOnStreamSelectedListener(new OnInfoItemGesture() { + itemTouchHelper = new ItemTouchHelper(getItemTouchCallback()); + itemTouchHelper.attachToRecyclerView(itemsList); + + itemListAdapter.setSelectedListener(new OnCustomItemGesture() { @Override - public void selected(StreamInfoItem selectedItem) { - // Requires the parent fragment to find holder for fragment replacement - NavigationHelper.openVideoDetailFragment(getFragmentManager(), - selectedItem.getServiceId(), selectedItem.url, selectedItem.getName()); + public void selected(LocalItem selectedItem) { + if (selectedItem instanceof PlaylistStreamEntry) { + final PlaylistStreamEntry item = (PlaylistStreamEntry) selectedItem; + NavigationHelper.openVideoDetailFragment(getFragmentManager(), + item.serviceId, item.url, item.title); + } } @Override - public void held(StreamInfoItem selectedItem) { - showStreamDialog(selectedItem); + public void held(LocalItem selectedItem) { + if (selectedItem instanceof PlaylistStreamEntry) { + showStreamDialog((PlaylistStreamEntry) selectedItem); + } } - }); + @Override + public void drag(LocalItem selectedItem, RecyclerView.ViewHolder viewHolder) { + if (itemTouchHelper != null) itemTouchHelper.startDrag(viewHolder); + } + }); + headerTitleView.setOnClickListener(view -> createRenameDialog()); } - @Override - protected void showStreamDialog(final StreamInfoItem item) { + protected void showStreamDialog(final PlaylistStreamEntry item) { final Context context = getContext(); final Activity activity = getActivity(); if (context == null || context.getResources() == null || getActivity() == null) return; + final StreamInfoItem infoItem = item.toStreamInfoItem(); + final String[] commands = new String[]{ context.getResources().getString(R.string.enqueue_on_background), context.getResources().getString(R.string.enqueue_on_popup), context.getResources().getString(R.string.start_here_on_main), context.getResources().getString(R.string.start_here_on_background), context.getResources().getString(R.string.start_here_on_popup), + "Set as Thumbnail", + context.getResources().getString(R.string.delete) }; final DialogInterface.OnClickListener actions = (dialogInterface, i) -> { - final int index = Math.max(infoListAdapter.getItemsList().indexOf(item), 0); + final int index = Math.max(itemListAdapter.getItemsList().indexOf(item), 0); switch (i) { case 0: - NavigationHelper.enqueueOnBackgroundPlayer(context, new SinglePlayQueue(item)); + NavigationHelper.enqueueOnBackgroundPlayer(context, + new SinglePlayQueue(infoItem)); break; case 1: - NavigationHelper.enqueueOnPopupPlayer(activity, new SinglePlayQueue(item)); + NavigationHelper.enqueueOnPopupPlayer(activity, new + SinglePlayQueue(infoItem)); break; case 2: NavigationHelper.playOnMainPlayer(context, getPlayQueue(index)); @@ -189,18 +213,56 @@ protected void showStreamDialog(final StreamInfoItem item) { case 4: NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(index)); break; + case 5: + changeThumbnailUrl(item.thumbnailUrl); + break; + case 6: + itemListAdapter.removeItemAt(index); + setVideoCount(itemListAdapter.getItemsList().size()); + break; default: break; } }; - new InfoItemDialog(getActivity(), item, commands, actions).show(); + new InfoItemDialog(getActivity(), infoItem, commands, actions).show(); + } + + private ItemTouchHelper.SimpleCallback getItemTouchCallback() { + return new ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP | ItemTouchHelper.DOWN, + ItemTouchHelper.ACTION_STATE_IDLE) { + @Override + public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder source, + RecyclerView.ViewHolder target) { + if (source.getItemViewType() != target.getItemViewType() || + itemListAdapter == null) { + return false; + } + + final int sourceIndex = source.getAdapterPosition(); + final int targetIndex = target.getAdapterPosition(); + return itemListAdapter.swapItems(sourceIndex, targetIndex); + } + + @Override + public boolean isLongPressDragEnabled() { + return false; + } + + @Override + public boolean isItemViewSwipeEnabled() { + return false; + } + + @Override + public void onSwiped(RecyclerView.ViewHolder viewHolder, int swipeDir) {} + }; } private void resetFragment() { if (disposables != null) disposables.clear(); if (databaseSubscription != null) databaseSubscription.cancel(); - if (infoListAdapter != null) infoListAdapter.clearStreamItemList(); + if (itemListAdapter != null) itemListAdapter.clearStreamItemList(); } /////////////////////////////////////////////////////////////////////////// @@ -224,8 +286,8 @@ public void startLoading(boolean forceLoad) { .subscribe(getPlaylistObserver()); } - private Subscriber> getPlaylistObserver() { - return new Subscriber>() { + private Subscriber> getPlaylistObserver() { + return new Subscriber>() { @Override public void onSubscribe(Subscription s) { showLoading(); @@ -236,7 +298,7 @@ public void onSubscribe(Subscription s) { } @Override - public void onNext(List streams) { + public void onNext(List streams) { handleResult(streams); if (databaseSubscription != null) databaseSubscription.request(1); } @@ -253,9 +315,9 @@ public void onComplete() { } @Override - public void handleResult(@NonNull List result) { + public void handleResult(@NonNull List result) { super.handleResult(result); - infoListAdapter.clearStreamItemList(); + itemListAdapter.clearStreamItemList(); if (result.isEmpty()) { showEmptyState(); @@ -265,15 +327,14 @@ public void handleResult(@NonNull List result) { animateView(headerRootLayout, true, 100); animateView(itemsList, true, 300); - infoListAdapter.addInfoItemList(getStreamItems(result)); + itemListAdapter.addInfoItemList(result); if (itemsListState != null) { itemsList.getLayoutManager().onRestoreInstanceState(itemsListState); itemsListState = null; } + setVideoCount(itemListAdapter.getItemsList().size()); playlistControl.setVisibility(View.VISIBLE); - headerStreamCount.setText( - getResources().getQuantityString(R.plurals.videos, result.size(), result.size())); headerPlayAllButton.setOnClickListener(view -> NavigationHelper.playOnMainPlayer(activity, getPlayQueue())); @@ -284,29 +345,6 @@ public void handleResult(@NonNull List result) { hideLoading(); } - - private List getStreamItems(final List streams) { - List items = new ArrayList<>(streams.size()); - for (final StreamEntity stream : streams) { - items.add(stream.toStreamEntityInfoItem()); - } - return items; - } - - /*////////////////////////////////////////////////////////////////////////// - // Contract - //////////////////////////////////////////////////////////////////////////*/ - - @Override - protected void loadMoreItems() { - // Do nothing - } - - @Override - protected boolean hasMoreItems() { - return false; - } - /////////////////////////////////////////////////////////////////////////// // Fragment Error Handling /////////////////////////////////////////////////////////////////////////// @@ -325,13 +363,13 @@ protected boolean onError(Throwable exception) { // Utils //////////////////////////////////////////////////////////////////////////*/ - protected void setInitialData(long playlistId, String name) { + private void setInitialData(long playlistId, String name) { this.playlistId = playlistId; this.name = !TextUtils.isEmpty(name) ? name : ""; } - protected void setFragmentTitle(final String title) { - if (activity.getSupportActionBar() != null) { + private void setFragmentTitle(final String title) { + if (activity != null && activity.getSupportActionBar() != null) { activity.getSupportActionBar().setTitle(title); } if (headerTitleView != null) { @@ -339,17 +377,80 @@ protected void setFragmentTitle(final String title) { } } + private void setVideoCount(final long count) { + if (activity != null && headerStreamCount != null) { + headerStreamCount.setText(Localization.localizeStreamCount(activity, count)); + } + } + private PlayQueue getPlayQueue() { return getPlayQueue(0); } private PlayQueue getPlayQueue(final int index) { - final List infoItems = infoListAdapter.getItemsList(); + final List infoItems = itemListAdapter.getItemsList(); List streamInfoItems = new ArrayList<>(infoItems.size()); - for (final InfoItem item : infoItems) { - if (item instanceof StreamInfoItem) streamInfoItems.add((StreamInfoItem) item); + for (final LocalItem item : infoItems) { + if (item instanceof PlaylistStreamEntry) { + streamInfoItems.add(((PlaylistStreamEntry) item).toStreamInfoItem()); + } } return new SinglePlayQueue(streamInfoItems, index); } + + private void createRenameDialog() { + if (playlistId == null || name == null || getContext() == null) return; + + final View dialogView = View.inflate(getContext(), R.layout.dialog_playlist_name, null); + EditText nameEdit = dialogView.findViewById(R.id.playlist_name); + nameEdit.setText(name); + nameEdit.setSelection(nameEdit.getText().length()); + + final AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(getContext()) + .setTitle(R.string.rename_playlist) + .setView(dialogView) + .setCancelable(true) + .setNegativeButton(R.string.cancel, null) + .setPositiveButton(R.string.create, (dialogInterface, i) -> { + name = nameEdit.getText().toString(); + setFragmentTitle(name); + + final LocalPlaylistManager playlistManager = + new LocalPlaylistManager(NewPipeDatabase.getInstance(getContext())); + final Toast successToast = Toast.makeText(getActivity(), + "Playlist renamed", + Toast.LENGTH_SHORT); + + playlistManager.renamePlaylist(playlistId, name) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(longs -> successToast.show()); + }); + + dialogBuilder.show(); + } + + private void changeThumbnailUrl(final String thumbnailUrl) { + final Toast successToast = Toast.makeText(getActivity(), + "Playlist thumbnail changed", + Toast.LENGTH_SHORT); + + playlistManager.changePlaylistThumbnail(playlistId, thumbnailUrl) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(ignore -> successToast.show()); + } + + private void saveJoin() { + final List items = itemListAdapter.getItemsList(); + List streamIds = new ArrayList<>(items.size()); + for (final LocalItem item : items) { + if (item instanceof PlaylistStreamEntry) { + streamIds.add(((PlaylistStreamEntry) item).streamId); + } + } + + playlistManager.updateJoin(playlistId, streamIds) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(); + } } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistManager.java b/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistManager.java index 4bc161c048a..c266f536549 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistManager.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/LocalPlaylistManager.java @@ -4,6 +4,7 @@ import org.schabi.newpipe.database.AppDatabase; import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry; +import org.schabi.newpipe.database.playlist.PlaylistStreamEntry; import org.schabi.newpipe.database.playlist.dao.PlaylistDAO; import org.schabi.newpipe.database.playlist.dao.PlaylistStreamDAO; import org.schabi.newpipe.database.playlist.model.PlaylistEntity; @@ -84,7 +85,7 @@ public Flowable> getPlaylists() { return playlistStreamTable.getPlaylistMetadata().subscribeOn(Schedulers.io()); } - public Flowable> getPlaylistStreams(final long playlistId) { + public Flowable> getPlaylistStreams(final long playlistId) { return playlistStreamTable.getOrderedStreamsOf(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 deleted file mode 100644 index 7862cf2f45c..00000000000 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/MostPlayedFragment.java +++ /dev/null @@ -1,35 +0,0 @@ -package org.schabi.newpipe.fragments.local; - -import org.schabi.newpipe.R; -import org.schabi.newpipe.database.stream.StreamStatisticsEntry; -import org.schabi.newpipe.extractor.InfoItem; -import org.schabi.newpipe.info_list.stored.StreamStatisticsInfoItem; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -public class MostPlayedFragment extends StatisticsPlaylistFragment { - @Override - protected String getName() { - return getString(R.string.title_most_played); - } - - @Override - protected List 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/local/OnCustomItemGesture.java b/app/src/main/java/org/schabi/newpipe/fragments/local/OnCustomItemGesture.java new file mode 100644 index 00000000000..0b65c595a86 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/OnCustomItemGesture.java @@ -0,0 +1,19 @@ +package org.schabi.newpipe.fragments.local; + +import android.support.v7.widget.RecyclerView; + +import org.schabi.newpipe.database.LocalItem; +import org.schabi.newpipe.extractor.InfoItem; + +public abstract class OnCustomItemGesture { + + public abstract void selected(T selectedItem); + + public void held(T selectedItem) { + // Optional gesture + } + + public void drag(T selectedItem, RecyclerView.ViewHolder viewHolder) { + // Optional gesture + } +} 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 deleted file mode 100644 index 2a4b8cfb081..00000000000 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/WatchHistoryFragment.java +++ /dev/null @@ -1,36 +0,0 @@ -package org.schabi.newpipe.fragments.local; - -import android.text.format.DateFormat; - -import org.schabi.newpipe.R; -import org.schabi.newpipe.database.stream.StreamStatisticsEntry; -import org.schabi.newpipe.extractor.InfoItem; -import org.schabi.newpipe.info_list.stored.StreamStatisticsInfoItem; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -public class WatchHistoryFragment extends StatisticsPlaylistFragment { - @Override - protected String getName() { - return getString(R.string.title_watch_history); - } - - @Override - protected List 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/fragments/local/BookmarkFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/BookmarkFragment.java similarity index 74% rename from app/src/main/java/org/schabi/newpipe/fragments/local/BookmarkFragment.java rename to app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/BookmarkFragment.java index 769365dd884..01b43305205 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/BookmarkFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/BookmarkFragment.java @@ -1,7 +1,7 @@ -package org.schabi.newpipe.fragments.local; +package org.schabi.newpipe.fragments.local.bookmark; +import android.app.AlertDialog; import android.content.Context; -import android.content.DialogInterface; import android.os.Bundle; import android.os.Parcelable; import android.support.annotation.NonNull; @@ -17,18 +17,15 @@ import org.reactivestreams.Subscription; import org.schabi.newpipe.NewPipeDatabase; import org.schabi.newpipe.R; +import org.schabi.newpipe.database.LocalItem; import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry; -import org.schabi.newpipe.extractor.InfoItem; -import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem; import org.schabi.newpipe.fragments.BaseStateFragment; -import org.schabi.newpipe.info_list.InfoItemDialog; -import org.schabi.newpipe.info_list.InfoListAdapter; -import org.schabi.newpipe.info_list.OnInfoItemGesture; -import org.schabi.newpipe.info_list.stored.LocalPlaylistInfoItem; +import org.schabi.newpipe.fragments.local.LocalItemListAdapter; +import org.schabi.newpipe.fragments.local.LocalPlaylistManager; +import org.schabi.newpipe.fragments.local.OnCustomItemGesture; import org.schabi.newpipe.report.UserAction; import org.schabi.newpipe.util.NavigationHelper; -import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -42,7 +39,7 @@ public class BookmarkFragment extends BaseStateFragment() { + itemListAdapter.setSelectedListener(new OnCustomItemGesture() { @Override - public void selected(PlaylistInfoItem selectedItem) { + public void selected(LocalItem selectedItem) { // Requires the parent fragment to find holder for fragment replacement - if (selectedItem instanceof LocalPlaylistInfoItem && getParentFragment() != null) { - final long playlistId = ((LocalPlaylistInfoItem) selectedItem).getPlaylistId(); - + if (selectedItem instanceof PlaylistMetadataEntry && getParentFragment() != null) { + final PlaylistMetadataEntry entry = ((PlaylistMetadataEntry) selectedItem); NavigationHelper.openLocalPlaylistFragment( - getParentFragment().getFragmentManager(), - playlistId, - selectedItem.getName() - ); + getParentFragment().getFragmentManager(), entry.uid, entry.name); } } @Override - public void held(PlaylistInfoItem selectedItem) { - if (selectedItem instanceof LocalPlaylistInfoItem) { - showPlaylistDialog((LocalPlaylistInfoItem) selectedItem); + public void held(LocalItem selectedItem) { + if (selectedItem instanceof PlaylistMetadataEntry) { + showDeleteDialog((PlaylistMetadataEntry) selectedItem); } } }); @@ -177,36 +168,25 @@ public void held(PlaylistInfoItem selectedItem) { }); } - private void showPlaylistDialog(final LocalPlaylistInfoItem item) { - final Context context = getContext(); - if (context == null || context.getResources() == null || getActivity() == null) return; - - final String[] commands = new String[]{ - context.getResources().getString(R.string.delete_playlist) - }; - - final DialogInterface.OnClickListener actions = (dialogInterface, i) -> { - switch (i) { - case 0: + private void showDeleteDialog(final PlaylistMetadataEntry item) { + new AlertDialog.Builder(activity) + .setTitle(item.name) + .setMessage(R.string.delete_playlist_prompt) + .setCancelable(true) + .setPositiveButton(R.string.delete, (dialog, i) -> { final Toast deleteSuccessful = Toast.makeText(getContext(), "Deleted", Toast.LENGTH_SHORT); - disposables.add(localPlaylistManager.deletePlaylist(item.getPlaylistId()) + disposables.add(localPlaylistManager.deletePlaylist(item.uid) .observeOn(AndroidSchedulers.mainThread()) .subscribe(ignored -> deleteSuccessful.show())); - break; - default: - break; - } - }; - - final String videoCount = getResources().getQuantityString(R.plurals.videos, - (int) item.getStreamCount(), (int) item.getStreamCount()); - new InfoItemDialog(getActivity(), commands, actions, item.getName(), videoCount).show(); + }) + .setNegativeButton(R.string.cancel, null) + .show(); } private void resetFragment() { if (disposables != null) disposables.clear(); - if (infoListAdapter != null) infoListAdapter.clearStreamItemList(); + if (itemListAdapter != null) itemListAdapter.clearStreamItemList(); } /////////////////////////////////////////////////////////////////////////// @@ -254,12 +234,12 @@ public void onComplete() { public void handleResult(@NonNull List result) { super.handleResult(result); - infoListAdapter.clearStreamItemList(); + itemListAdapter.clearStreamItemList(); if (result.isEmpty()) { showEmptyState(); } else { - infoListAdapter.addInfoItemList(infoItemsOf(result)); + itemListAdapter.addInfoItemList(infoItemsOf(result)); if (itemsListState != null) { itemsList.getLayoutManager().onRestoreInstanceState(itemsListState); itemsListState = null; @@ -269,13 +249,9 @@ public void handleResult(@NonNull List result) { } - 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; + private List infoItemsOf(List playlists) { + Collections.sort(playlists, (o1, o2) -> o1.name.compareToIgnoreCase(o2.name)); + return playlists; } /*////////////////////////////////////////////////////////////////////////// diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/MostPlayedFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/MostPlayedFragment.java new file mode 100644 index 00000000000..ed0d903a82a --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/MostPlayedFragment.java @@ -0,0 +1,22 @@ +package org.schabi.newpipe.fragments.local.bookmark; + +import org.schabi.newpipe.R; +import org.schabi.newpipe.database.stream.StreamStatisticsEntry; + +import java.util.Collections; +import java.util.List; + +public class MostPlayedFragment extends StatisticsPlaylistFragment { + @Override + protected String getName() { + return getString(R.string.title_most_played); + } + + @Override + protected List processResult(List results) { + Collections.sort(results, (left, right) -> + ((Long) right.watchCount).compareTo(left.watchCount)); + return results; + } + +} diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/StatisticsPlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/StatisticsPlaylistFragment.java similarity index 80% rename from app/src/main/java/org/schabi/newpipe/fragments/local/StatisticsPlaylistFragment.java rename to app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/StatisticsPlaylistFragment.java index 6eddc3a5cc8..5c2959d9ca1 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/local/StatisticsPlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/StatisticsPlaylistFragment.java @@ -1,4 +1,4 @@ -package org.schabi.newpipe.fragments.local; +package org.schabi.newpipe.fragments.local.bookmark; import android.app.Activity; import android.content.Context; @@ -14,14 +14,13 @@ import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; import org.schabi.newpipe.R; +import org.schabi.newpipe.database.LocalItem; import org.schabi.newpipe.database.stream.StreamStatisticsEntry; -import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.stream.StreamInfoItem; -import org.schabi.newpipe.fragments.list.BaseListFragment; +import org.schabi.newpipe.fragments.local.BaseLocalListFragment; +import org.schabi.newpipe.fragments.local.OnCustomItemGesture; import org.schabi.newpipe.history.HistoryRecordManager; import org.schabi.newpipe.info_list.InfoItemDialog; -import org.schabi.newpipe.info_list.OnInfoItemGesture; -import org.schabi.newpipe.info_list.stored.StreamStatisticsInfoItem; import org.schabi.newpipe.playlist.PlayQueue; import org.schabi.newpipe.playlist.SinglePlayQueue; import org.schabi.newpipe.report.UserAction; @@ -36,7 +35,7 @@ import static org.schabi.newpipe.util.AnimationUtils.animateView; public abstract class StatisticsPlaylistFragment - extends BaseListFragment, Void> { + extends BaseLocalListFragment, Void> { private View headerRootLayout; private View playlistControl; @@ -57,9 +56,7 @@ public abstract class StatisticsPlaylistFragment protected abstract String getName(); - protected abstract List processResult(final List results); - - protected abstract String getAdditionalDetail(final StreamStatisticsInfoItem infoItem); + protected abstract List processResult(final List results); /////////////////////////////////////////////////////////////////////////// // Fragment LifeCycle @@ -106,8 +103,6 @@ public void onDestroy() { @Override protected void initViews(View rootView, Bundle savedInstanceState) { super.initViews(rootView, savedInstanceState); - infoListAdapter.useMiniItemVariants(true); - setFragmentTitle(getName()); } @@ -127,27 +122,31 @@ protected View getListHeader() { protected void initListeners() { super.initListeners(); - infoListAdapter.setOnStreamSelectedListener(new OnInfoItemGesture() { + itemListAdapter.setSelectedListener(new OnCustomItemGesture() { @Override - public void selected(StreamInfoItem selectedItem) { - NavigationHelper.openVideoDetailFragment(getFragmentManager(), - selectedItem.getServiceId(), selectedItem.url, selectedItem.getName()); + public void selected(LocalItem selectedItem) { + if (selectedItem instanceof StreamStatisticsEntry) { + final StreamStatisticsEntry item = (StreamStatisticsEntry) selectedItem; + NavigationHelper.openVideoDetailFragment(getFragmentManager(), + item.serviceId, item.url, item.title); + } } @Override - public void held(StreamInfoItem selectedItem) { - showStreamDialog(selectedItem); + public void held(LocalItem selectedItem) { + if (selectedItem instanceof StreamStatisticsEntry) { + showStreamDialog((StreamStatisticsEntry) selectedItem); + } } }); } - @Override - protected void showStreamDialog(final StreamInfoItem item) { + private void showStreamDialog(final StreamStatisticsEntry item) { final Context context = getContext(); final Activity activity = getActivity(); - if (context == null || context.getResources() == null - || getActivity() == null || !(item instanceof StreamStatisticsInfoItem)) return; + if (context == null || context.getResources() == null || getActivity() == null) return; + final StreamInfoItem infoItem = item.toStreamInfoItem(); final String[] commands = new String[]{ context.getResources().getString(R.string.enqueue_on_background), @@ -158,13 +157,13 @@ protected void showStreamDialog(final StreamInfoItem item) { }; final DialogInterface.OnClickListener actions = (dialogInterface, i) -> { - final int index = Math.max(infoListAdapter.getItemsList().indexOf(item), 0); + final int index = Math.max(itemListAdapter.getItemsList().indexOf(item), 0); switch (i) { case 0: - NavigationHelper.enqueueOnBackgroundPlayer(context, new SinglePlayQueue(item)); + NavigationHelper.enqueueOnBackgroundPlayer(context, new SinglePlayQueue(infoItem)); break; case 1: - NavigationHelper.enqueueOnPopupPlayer(activity, new SinglePlayQueue(item)); + NavigationHelper.enqueueOnPopupPlayer(activity, new SinglePlayQueue(infoItem)); break; case 2: NavigationHelper.playOnMainPlayer(context, getPlayQueue(index)); @@ -180,13 +179,12 @@ protected void showStreamDialog(final StreamInfoItem item) { } }; - final String detail = getAdditionalDetail((StreamStatisticsInfoItem) item); - new InfoItemDialog(getActivity(), commands, actions, item.getName(), detail).show(); + new InfoItemDialog(getActivity(), infoItem, commands, actions).show(); } private void resetFragment() { if (databaseSubscription != null) databaseSubscription.cancel(); - if (infoListAdapter != null) infoListAdapter.clearStreamItemList(); + if (itemListAdapter != null) itemListAdapter.clearStreamItemList(); } /////////////////////////////////////////////////////////////////////////// @@ -241,7 +239,7 @@ public void onComplete() { @Override public void handleResult(@NonNull List result) { super.handleResult(result); - infoListAdapter.clearStreamItemList(); + itemListAdapter.clearStreamItemList(); if (result.isEmpty()) { showEmptyState(); @@ -251,7 +249,7 @@ public void handleResult(@NonNull List result) { animateView(headerRootLayout, true, 100); animateView(itemsList, true, 300); - infoListAdapter.addInfoItemList(processResult(result)); + itemListAdapter.addInfoItemList(processResult(result)); if (itemsListState != null) { itemsList.getLayoutManager().onRestoreInstanceState(itemsListState); itemsListState = null; @@ -267,20 +265,6 @@ public void handleResult(@NonNull List result) { hideLoading(); } - /*////////////////////////////////////////////////////////////////////////// - // Contract - //////////////////////////////////////////////////////////////////////////*/ - - @Override - protected void loadMoreItems() { - // Do nothing - } - - @Override - protected boolean hasMoreItems() { - return false; - } - /////////////////////////////////////////////////////////////////////////// // Fragment Error Handling /////////////////////////////////////////////////////////////////////////// @@ -310,10 +294,12 @@ private PlayQueue getPlayQueue() { } private PlayQueue getPlayQueue(final int index) { - final List infoItems = infoListAdapter.getItemsList(); + final List infoItems = itemListAdapter.getItemsList(); List streamInfoItems = new ArrayList<>(infoItems.size()); - for (final InfoItem item : infoItems) { - if (item instanceof StreamInfoItem) streamInfoItems.add((StreamInfoItem) item); + for (final LocalItem item : infoItems) { + if (item instanceof StreamStatisticsEntry) { + streamInfoItems.add(((StreamStatisticsEntry) item).toStreamInfoItem()); + } } return new SinglePlayQueue(streamInfoItems, index); } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/WatchHistoryFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/WatchHistoryFragment.java new file mode 100644 index 00000000000..853029ae68d --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/bookmark/WatchHistoryFragment.java @@ -0,0 +1,21 @@ +package org.schabi.newpipe.fragments.local.bookmark; + +import org.schabi.newpipe.R; +import org.schabi.newpipe.database.stream.StreamStatisticsEntry; + +import java.util.Collections; +import java.util.List; + +public class WatchHistoryFragment extends StatisticsPlaylistFragment { + @Override + protected String getName() { + return getString(R.string.title_watch_history); + } + + @Override + protected List processResult(List results) { + Collections.sort(results, (left, right) -> + right.latestAccessDate.compareTo(left.latestAccessDate)); + return results; + } +} diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/holder/LocalItemHolder.java b/app/src/main/java/org/schabi/newpipe/fragments/local/holder/LocalItemHolder.java new file mode 100644 index 00000000000..64dc8447242 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/holder/LocalItemHolder.java @@ -0,0 +1,56 @@ +package org.schabi.newpipe.fragments.local.holder; + +import android.support.v7.widget.RecyclerView; +import android.view.LayoutInflater; +import android.view.ViewGroup; + +import com.nostra13.universalimageloader.core.DisplayImageOptions; + +import org.schabi.newpipe.database.LocalItem; +import org.schabi.newpipe.fragments.local.LocalItemBuilder; + +import java.text.DateFormat; + +/* + * Created by Christian Schabesberger on 12.02.17. + * + * Copyright (C) Christian Schabesberger 2016 + * InfoItemHolder.java is part of NewPipe. + * + * NewPipe is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * NewPipe is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with NewPipe. If not, see . + */ + +public abstract class LocalItemHolder extends RecyclerView.ViewHolder { + protected final LocalItemBuilder itemBuilder; + + public LocalItemHolder(LocalItemBuilder itemBuilder, int layoutId, ViewGroup parent) { + super(LayoutInflater.from(itemBuilder.getContext()) + .inflate(layoutId, parent, false)); + this.itemBuilder = itemBuilder; + } + + public abstract void updateFromItem(final LocalItem item, final DateFormat dateFormat); + + /*////////////////////////////////////////////////////////////////////////// + // ImageLoaderOptions + //////////////////////////////////////////////////////////////////////////*/ + + /** + * Base display options + */ + public static final DisplayImageOptions BASE_DISPLAY_IMAGE_OPTIONS = + new DisplayImageOptions.Builder() + .cacheInMemory(true) + .build(); +} diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/holder/LocalPlaylistItemHolder.java b/app/src/main/java/org/schabi/newpipe/fragments/local/holder/LocalPlaylistItemHolder.java new file mode 100644 index 00000000000..d04fc123a7b --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/holder/LocalPlaylistItemHolder.java @@ -0,0 +1,74 @@ +package org.schabi.newpipe.fragments.local.holder; + +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; + +import com.nostra13.universalimageloader.core.DisplayImageOptions; + +import org.schabi.newpipe.R; +import org.schabi.newpipe.database.LocalItem; +import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry; +import org.schabi.newpipe.fragments.local.LocalItemBuilder; + +import java.text.DateFormat; + +public class LocalPlaylistItemHolder extends LocalItemHolder { + public final ImageView itemThumbnailView; + public final TextView itemStreamCountView; + public final TextView itemTitleView; + public final TextView itemUploaderView; + + public LocalPlaylistItemHolder(LocalItemBuilder infoItemBuilder, + int layoutId, ViewGroup parent) { + super(infoItemBuilder, layoutId, parent); + + itemThumbnailView = itemView.findViewById(R.id.itemThumbnailView); + itemTitleView = itemView.findViewById(R.id.itemTitleView); + itemStreamCountView = itemView.findViewById(R.id.itemStreamCountView); + itemUploaderView = itemView.findViewById(R.id.itemUploaderView); + } + + public LocalPlaylistItemHolder(LocalItemBuilder infoItemBuilder, ViewGroup parent) { + this(infoItemBuilder, R.layout.list_playlist_mini_item, parent); + } + + @Override + public void updateFromItem(final LocalItem localItem, final DateFormat dateFormat) { + if (!(localItem instanceof PlaylistMetadataEntry)) return; + final PlaylistMetadataEntry item = (PlaylistMetadataEntry) localItem; + + itemTitleView.setText(item.name); + itemStreamCountView.setText(String.valueOf(item.streamCount)); + itemUploaderView.setVisibility(View.INVISIBLE); + + itemBuilder.getImageLoader().displayImage(item.thumbnailUrl, itemThumbnailView, + DISPLAY_THUMBNAIL_OPTIONS); + + itemView.setOnClickListener(view -> { + if (itemBuilder.getOnItemSelectedListener() != null) { + itemBuilder.getOnItemSelectedListener().selected(item); + } + }); + + itemView.setLongClickable(true); + itemView.setOnLongClickListener(view -> { + if (itemBuilder.getOnItemSelectedListener() != null) { + itemBuilder.getOnItemSelectedListener().held(item); + } + return true; + }); + } + + /** + * Display options for playlist thumbnails + */ + public static final DisplayImageOptions DISPLAY_THUMBNAIL_OPTIONS = + new DisplayImageOptions.Builder() + .cloneFrom(BASE_DISPLAY_IMAGE_OPTIONS) + .showImageOnLoading(R.drawable.dummy_thumbnail_playlist) + .showImageForEmptyUri(R.drawable.dummy_thumbnail_playlist) + .showImageOnFail(R.drawable.dummy_thumbnail_playlist) + .build(); +} diff --git a/app/src/main/java/org/schabi/newpipe/info_list/holder/StreamPlaylistInfoItemHolder.java b/app/src/main/java/org/schabi/newpipe/fragments/local/holder/LocalPlaylistStreamItemHolder.java similarity index 58% rename from app/src/main/java/org/schabi/newpipe/info_list/holder/StreamPlaylistInfoItemHolder.java rename to app/src/main/java/org/schabi/newpipe/fragments/local/holder/LocalPlaylistStreamItemHolder.java index 8261d47606f..712db8f8a58 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/holder/StreamPlaylistInfoItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/holder/LocalPlaylistStreamItemHolder.java @@ -1,7 +1,6 @@ -package org.schabi.newpipe.info_list.holder; +package org.schabi.newpipe.fragments.local.holder; import android.support.v4.content.ContextCompat; -import android.support.v7.widget.RecyclerView; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; @@ -11,40 +10,44 @@ import com.nostra13.universalimageloader.core.DisplayImageOptions; import org.schabi.newpipe.R; -import org.schabi.newpipe.extractor.InfoItem; -import org.schabi.newpipe.extractor.stream.StreamInfoItem; -import org.schabi.newpipe.info_list.InfoItemBuilder; +import org.schabi.newpipe.database.LocalItem; +import org.schabi.newpipe.database.playlist.PlaylistStreamEntry; +import org.schabi.newpipe.extractor.NewPipe; +import org.schabi.newpipe.fragments.local.LocalItemBuilder; import org.schabi.newpipe.util.Localization; -public class StreamPlaylistInfoItemHolder extends InfoItemHolder { +import java.text.DateFormat; + +public class LocalPlaylistStreamItemHolder extends LocalItemHolder { public final ImageView itemThumbnailView; public final TextView itemVideoTitleView; - public final TextView itemUploaderView; + public final TextView itemAdditionalDetailsView; public final TextView itemDurationView; public final View itemHandleView; - StreamPlaylistInfoItemHolder(InfoItemBuilder infoItemBuilder, int layoutId, ViewGroup parent) { + LocalPlaylistStreamItemHolder(LocalItemBuilder infoItemBuilder, int layoutId, ViewGroup parent) { super(infoItemBuilder, layoutId, parent); itemThumbnailView = itemView.findViewById(R.id.itemThumbnailView); itemVideoTitleView = itemView.findViewById(R.id.itemVideoTitleView); - itemUploaderView = itemView.findViewById(R.id.itemUploaderView); + itemAdditionalDetailsView = itemView.findViewById(R.id.itemAdditionalDetails); itemDurationView = itemView.findViewById(R.id.itemDurationView); itemHandleView = itemView.findViewById(R.id.itemHandle); } - public StreamPlaylistInfoItemHolder(InfoItemBuilder infoItemBuilder, ViewGroup parent) { - this(infoItemBuilder, R.layout.list_playlist_mini_item, parent); + public LocalPlaylistStreamItemHolder(LocalItemBuilder infoItemBuilder, ViewGroup parent) { + this(infoItemBuilder, R.layout.list_stream_playlist_item, parent); } @Override - public void updateFromItem(final InfoItem infoItem) { - if (!(infoItem instanceof StreamInfoItem)) return; - final StreamInfoItem item = (StreamInfoItem) infoItem; + public void updateFromItem(final LocalItem localItem, final DateFormat dateFormat) { + if (!(localItem instanceof PlaylistStreamEntry)) return; + final PlaylistStreamEntry item = (PlaylistStreamEntry) localItem; - itemVideoTitleView.setText(item.getName()); - itemUploaderView.setText(item.uploader_name); + itemVideoTitleView.setText(item.title); + itemAdditionalDetailsView.setText(Localization.concatenateStrings(item.uploader, + NewPipe.getNameOfService(item.serviceId))); if (item.duration > 0) { itemDurationView.setText(Localization.getDurationString(item.duration)); @@ -56,19 +59,19 @@ public void updateFromItem(final InfoItem infoItem) { } // Default thumbnail is shown on error, while loading and if the url is empty - itemBuilder.getImageLoader().displayImage(item.thumbnail_url, itemThumbnailView, - StreamPlaylistInfoItemHolder.DISPLAY_THUMBNAIL_OPTIONS); + itemBuilder.getImageLoader().displayImage(item.thumbnailUrl, itemThumbnailView, + LocalPlaylistStreamItemHolder.DISPLAY_THUMBNAIL_OPTIONS); itemView.setOnClickListener(view -> { - if (itemBuilder.getOnStreamSelectedListener() != null) { - itemBuilder.getOnStreamSelectedListener().selected(item); + if (itemBuilder.getOnItemSelectedListener() != null) { + itemBuilder.getOnItemSelectedListener().selected(item); } }); itemView.setLongClickable(true); itemView.setOnLongClickListener(view -> { - if (itemBuilder.getOnStreamSelectedListener() != null) { - itemBuilder.getOnStreamSelectedListener().held(item); + if (itemBuilder.getOnItemSelectedListener() != null) { + itemBuilder.getOnItemSelectedListener().held(item); } return true; }); @@ -77,13 +80,13 @@ public void updateFromItem(final InfoItem infoItem) { itemHandleView.setOnTouchListener(getOnTouchListener(item)); } - private View.OnTouchListener getOnTouchListener(final StreamInfoItem item) { + private View.OnTouchListener getOnTouchListener(final PlaylistStreamEntry item) { return (view, motionEvent) -> { view.performClick(); if (itemBuilder != null && motionEvent.getActionMasked() == MotionEvent.ACTION_DOWN) { - itemBuilder.getOnStreamSelectedListener() - .drag(item, StreamPlaylistInfoItemHolder.this); + itemBuilder.getOnItemSelectedListener().drag(item, + LocalPlaylistStreamItemHolder.this); } return false; }; diff --git a/app/src/main/java/org/schabi/newpipe/fragments/local/holder/LocalStatisticStreamItemHolder.java b/app/src/main/java/org/schabi/newpipe/fragments/local/holder/LocalStatisticStreamItemHolder.java new file mode 100644 index 00000000000..bce6bab7659 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/local/holder/LocalStatisticStreamItemHolder.java @@ -0,0 +1,119 @@ +package org.schabi.newpipe.fragments.local.holder; + +import android.support.v4.content.ContextCompat; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; + +import com.nostra13.universalimageloader.core.DisplayImageOptions; + +import org.schabi.newpipe.R; +import org.schabi.newpipe.database.LocalItem; +import org.schabi.newpipe.database.stream.StreamStatisticsEntry; +import org.schabi.newpipe.extractor.NewPipe; +import org.schabi.newpipe.fragments.local.LocalItemBuilder; +import org.schabi.newpipe.util.Localization; + +import java.text.DateFormat; + +/* + * Created by Christian Schabesberger on 01.08.16. + *

+ * Copyright (C) Christian Schabesberger 2016 + * StreamInfoItemHolder.java is part of NewPipe. + *

+ * NewPipe is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + *

+ * NewPipe is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + *

+ * You should have received a copy of the GNU General Public License + * along with NewPipe. If not, see . + */ + +public class LocalStatisticStreamItemHolder extends LocalItemHolder { + + public final ImageView itemThumbnailView; + public final TextView itemVideoTitleView; + public final TextView itemUploaderView; + public final TextView itemDurationView; + public final TextView itemAdditionalDetails; + + LocalStatisticStreamItemHolder(LocalItemBuilder infoItemBuilder, int layoutId, ViewGroup parent) { + super(infoItemBuilder, layoutId, parent); + + itemThumbnailView = itemView.findViewById(R.id.itemThumbnailView); + itemVideoTitleView = itemView.findViewById(R.id.itemVideoTitleView); + itemUploaderView = itemView.findViewById(R.id.itemUploaderView); + itemDurationView = itemView.findViewById(R.id.itemDurationView); + itemAdditionalDetails = itemView.findViewById(R.id.itemAdditionalDetails); + } + + public LocalStatisticStreamItemHolder(LocalItemBuilder infoItemBuilder, ViewGroup parent) { + this(infoItemBuilder, R.layout.list_stream_item, parent); + } + + private String getStreamInfoDetailLine(final StreamStatisticsEntry entry, + final DateFormat dateFormat) { + final String watchCount = Localization.shortViewCount(itemBuilder.getContext(), + entry.watchCount); + final String uploadDate = dateFormat.format(entry.latestAccessDate); + final String serviceName = NewPipe.getNameOfService(entry.serviceId); + return Localization.concatenateStrings(watchCount, uploadDate, serviceName); + } + + @Override + public void updateFromItem(final LocalItem localItem, final DateFormat dateFormat) { + if (!(localItem instanceof StreamStatisticsEntry)) return; + final StreamStatisticsEntry item = (StreamStatisticsEntry) localItem; + + itemVideoTitleView.setText(item.title); + itemUploaderView.setText(item.uploader); + + if (item.duration > 0) { + itemDurationView.setText(Localization.getDurationString(item.duration)); + itemDurationView.setBackgroundColor(ContextCompat.getColor(itemBuilder.getContext(), + R.color.duration_background_color)); + itemDurationView.setVisibility(View.VISIBLE); + } else { + itemDurationView.setVisibility(View.GONE); + } + + itemAdditionalDetails.setText(getStreamInfoDetailLine(item, dateFormat)); + + // Default thumbnail is shown on error, while loading and if the url is empty + itemBuilder.getImageLoader().displayImage(item.thumbnailUrl, itemThumbnailView, + DISPLAY_THUMBNAIL_OPTIONS); + + itemView.setOnClickListener(view -> { + if (itemBuilder.getOnItemSelectedListener() != null) { + itemBuilder.getOnItemSelectedListener().selected(item); + } + }); + + itemView.setLongClickable(true); + itemView.setOnLongClickListener(view -> { + if (itemBuilder.getOnItemSelectedListener() != null) { + itemBuilder.getOnItemSelectedListener().held(item); + } + return true; + }); + } + + /** + * Display options for stream thumbnails + */ + public static final DisplayImageOptions DISPLAY_THUMBNAIL_OPTIONS = + new DisplayImageOptions.Builder() + .cloneFrom(BASE_DISPLAY_IMAGE_OPTIONS) + .showImageOnFail(R.drawable.dummy_thumbnail) + .showImageForEmptyUri(R.drawable.dummy_thumbnail) + .showImageOnLoading(R.drawable.dummy_thumbnail) + .build(); +} diff --git a/app/src/main/java/org/schabi/newpipe/history/HistoryFragment.java b/app/src/main/java/org/schabi/newpipe/history/HistoryFragment.java index 462c12e61a1..5369c657c48 100644 --- a/app/src/main/java/org/schabi/newpipe/history/HistoryFragment.java +++ b/app/src/main/java/org/schabi/newpipe/history/HistoryFragment.java @@ -32,6 +32,7 @@ import io.reactivex.Single; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.CompositeDisposable; +import io.reactivex.disposables.Disposable; import static org.schabi.newpipe.util.AnimationUtils.animateView; @@ -169,8 +170,14 @@ protected void makeSnackbar(@StringRes final int text) { private void clearHistory() { final Collection itemsToDelete = new ArrayList<>(mHistoryAdapter.getItems()); - disposables.add(delete(itemsToDelete).observeOn(AndroidSchedulers.mainThread()) - .subscribe()); + + final Disposable deletion = delete(itemsToDelete) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(); + final Disposable cleanUp = historyRecordManager.removeOrphanedRecords() + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(); + disposables.addAll(deletion, cleanUp); makeSnackbar(R.string.history_cleared); mHistoryAdapter.clear(); diff --git a/app/src/main/java/org/schabi/newpipe/history/HistoryRecordManager.java b/app/src/main/java/org/schabi/newpipe/history/HistoryRecordManager.java index 1a5fe0525ae..9d9b74b301f 100644 --- a/app/src/main/java/org/schabi/newpipe/history/HistoryRecordManager.java +++ b/app/src/main/java/org/schabi/newpipe/history/HistoryRecordManager.java @@ -48,6 +48,14 @@ public HistoryRecordManager(final Context context) { streamHistoryKey = context.getString(R.string.enable_watch_history_key); } + public Single removeOrphanedRecords() { + return Single.fromCallable(streamTable::deleteOrphans).subscribeOn(Schedulers.io()); + } + + /////////////////////////////////////////////////////// + // Watch History + /////////////////////////////////////////////////////// + public Maybe onViewed(final StreamInfo info) { if (!isStreamHistoryEnabled()) return Maybe.empty(); 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 1dc4442c76f..dbf5d75563a 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 @@ -55,10 +55,6 @@ public class InfoListAdapter extends RecyclerView.Adapter infoItemList; private boolean useMiniVariant = false; diff --git a/app/src/main/java/org/schabi/newpipe/info_list/OnInfoItemGesture.java b/app/src/main/java/org/schabi/newpipe/info_list/OnInfoItemGesture.java index 3e6fe221308..84634c1d95d 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/OnInfoItemGesture.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/OnInfoItemGesture.java @@ -1,7 +1,5 @@ package org.schabi.newpipe.info_list; -import android.support.v7.widget.RecyclerView; - import org.schabi.newpipe.extractor.InfoItem; public abstract class OnInfoItemGesture { @@ -11,8 +9,4 @@ public abstract class OnInfoItemGesture { public void held(T selectedItem) { // Optional gesture } - - public void drag(T selectedItem, RecyclerView.ViewHolder viewHolder) { - // Optional gesture - } } diff --git a/app/src/main/java/org/schabi/newpipe/info_list/stored/StreamEntityInfoItem.java b/app/src/main/java/org/schabi/newpipe/info_list/stored/StreamEntityInfoItem.java deleted file mode 100644 index a54135211af..00000000000 --- a/app/src/main/java/org/schabi/newpipe/info_list/stored/StreamEntityInfoItem.java +++ /dev/null @@ -1,18 +0,0 @@ -package org.schabi.newpipe.info_list.stored; - -import org.schabi.newpipe.extractor.stream.StreamInfoItem; -import org.schabi.newpipe.extractor.stream.StreamType; - -public class StreamEntityInfoItem extends StreamInfoItem { - protected final long streamId; - - public StreamEntityInfoItem(final long streamId, final int serviceId, - final String url, final String name, final StreamType type) { - super(serviceId, url, name, type); - this.streamId = streamId; - } - - public long getStreamId() { - return streamId; - } -} 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 deleted file mode 100644 index 6659b551aae..00000000000 --- a/app/src/main/java/org/schabi/newpipe/info_list/stored/StreamStatisticsInfoItem.java +++ /dev/null @@ -1,31 +0,0 @@ -package org.schabi.newpipe.info_list.stored; - -import org.schabi.newpipe.extractor.stream.StreamType; - -import java.util.Date; - -public final class StreamStatisticsInfoItem extends StreamEntityInfoItem { - private Date latestAccessDate; - private long watchCount; - - public StreamStatisticsInfoItem(final long streamId, final int serviceId, - final String url, final String name, final StreamType type) { - super(streamId, serviceId, url, name, type); - } - - public Date getLatestAccessDate() { - return latestAccessDate; - } - - public void setLatestAccessDate(Date latestAccessDate) { - this.latestAccessDate = latestAccessDate; - } - - public long getWatchCount() { - return watchCount; - } - - public void setWatchCount(long watchCount) { - this.watchCount = watchCount; - } -} diff --git a/app/src/main/java/org/schabi/newpipe/util/Localization.java b/app/src/main/java/org/schabi/newpipe/util/Localization.java index 43ebc16776d..c1e5c9ed4ae 100644 --- a/app/src/main/java/org/schabi/newpipe/util/Localization.java +++ b/app/src/main/java/org/schabi/newpipe/util/Localization.java @@ -4,6 +4,7 @@ import android.content.SharedPreferences; import android.content.res.Resources; import android.preference.PreferenceManager; +import android.support.annotation.NonNull; import android.support.annotation.PluralsRes; import android.support.annotation.StringRes; import android.text.TextUtils; @@ -14,7 +15,9 @@ import java.text.NumberFormat; import java.text.ParseException; import java.text.SimpleDateFormat; +import java.util.Arrays; import java.util.Date; +import java.util.List; import java.util.Locale; /* @@ -39,9 +42,33 @@ public class Localization { + public final static String DOT_SEPARATOR = " • "; + private Localization() { } + @NonNull + public static String concatenateStrings(final String... strings) { + return concatenateStrings(Arrays.asList(strings)); + } + + @NonNull + public static String concatenateStrings(final List strings) { + if (strings.isEmpty()) return ""; + + final StringBuilder stringBuilder = new StringBuilder(); + stringBuilder.append(strings.get(0)); + + for (int i = 1; i < strings.size(); i++) { + final String string = strings.get(i); + if (!TextUtils.isEmpty(string)) { + stringBuilder.append(DOT_SEPARATOR).append(strings.get(i)); + } + } + + return stringBuilder.toString(); + } + public static Locale getPreferredLocale(Context context) { SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context); 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 60b94731dec..a32b1d47765 100644 --- a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java @@ -35,8 +35,8 @@ import org.schabi.newpipe.fragments.list.playlist.PlaylistFragment; import org.schabi.newpipe.fragments.list.search.SearchFragment; import org.schabi.newpipe.fragments.local.LocalPlaylistFragment; -import org.schabi.newpipe.fragments.local.MostPlayedFragment; -import org.schabi.newpipe.fragments.local.WatchHistoryFragment; +import org.schabi.newpipe.fragments.local.bookmark.MostPlayedFragment; +import org.schabi.newpipe.fragments.local.bookmark.WatchHistoryFragment; import org.schabi.newpipe.history.HistoryActivity; import org.schabi.newpipe.player.BackgroundPlayer; import org.schabi.newpipe.player.BackgroundPlayerActivity; diff --git a/app/src/main/res/layout/list_stream_playlist_item.xml b/app/src/main/res/layout/list_stream_playlist_item.xml index 3a5b1b8e6a9..193b3fea49f 100644 --- a/app/src/main/res/layout/list_stream_playlist_item.xml +++ b/app/src/main/res/layout/list_stream_playlist_item.xml @@ -70,7 +70,7 @@ tools:text="Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc tristique..."/> + tools:text="Mix musics #23 title Lorem ipsum dolor sit amet, consectetur..." /> - + android:visible="true" + app:showAsAction="ifRoom"/> Delete One Delete All Checksum + Dismiss New mission @@ -371,6 +372,8 @@ Create New Playlist Delete Playlist + Rename Playlist Name Add To Playlist + Do you want to delete this playlist?