Merge remote-tracking branch 'origin/1473_remove_duplicates_from_playlist' into 1473_remove_duplicates_from_playlist

This commit is contained in:
Jared Fantaye 2023-02-28 16:44:13 +01:00
commit 265de55a07
308 changed files with 5310 additions and 1395 deletions

View file

@ -22,10 +22,11 @@ import org.schabi.newpipe.R;
import org.schabi.newpipe.databinding.PignateFooterBinding;
import org.schabi.newpipe.fragments.BaseStateFragment;
import org.schabi.newpipe.fragments.list.ListViewContract;
import org.schabi.newpipe.info_list.ItemViewMode;
import static org.schabi.newpipe.ktx.ViewUtils.animate;
import static org.schabi.newpipe.ktx.ViewUtils.animateHideRecyclerViewAllowingScrolling;
import static org.schabi.newpipe.util.ThemeHelper.shouldUseGridLayout;
import static org.schabi.newpipe.util.ThemeHelper.getItemViewMode;
/**
* This fragment is design to be used with persistent data such as
@ -77,16 +78,23 @@ public abstract class BaseLocalListFragment<I, N> extends BaseStateFragment<I>
super.onResume();
if (updateFlags != 0) {
if ((updateFlags & LIST_MODE_UPDATE_FLAG) != 0) {
final boolean useGrid = shouldUseGridLayout(requireContext());
itemsList.setLayoutManager(
useGrid ? getGridLayoutManager() : getListLayoutManager());
itemListAdapter.setUseGridVariant(useGrid);
itemListAdapter.notifyDataSetChanged();
refreshItemViewMode();
}
updateFlags = 0;
}
}
/**
* Updates the item view mode based on user preference.
*/
private void refreshItemViewMode() {
final ItemViewMode itemViewMode = getItemViewMode(requireContext());
itemsList.setLayoutManager((itemViewMode == ItemViewMode.GRID)
? getGridLayoutManager() : getListLayoutManager());
itemListAdapter.setItemViewMode(itemViewMode);
itemListAdapter.notifyDataSetChanged();
}
/*//////////////////////////////////////////////////////////////////////////
// Lifecycle - View
//////////////////////////////////////////////////////////////////////////*/
@ -120,11 +128,9 @@ public abstract class BaseLocalListFragment<I, N> extends BaseStateFragment<I>
itemListAdapter = new LocalItemListAdapter(activity);
final boolean useGrid = shouldUseGridLayout(requireContext());
itemsList = rootView.findViewById(R.id.items_list);
itemsList.setLayoutManager(useGrid ? getGridLayoutManager() : getListLayoutManager());
refreshItemViewMode();
itemListAdapter.setUseGridVariant(useGrid);
headerRootBinding = getListHeader();
if (headerRootBinding != null) {
itemListAdapter.setHeader(headerRootBinding.getRoot());
@ -255,7 +261,7 @@ public abstract class BaseLocalListFragment<I, N> extends BaseStateFragment<I>
@Override
public void onSharedPreferenceChanged(final SharedPreferences sharedPreferences,
final String key) {
if (key.equals(getString(R.string.list_view_mode_key))) {
if (getString(R.string.list_view_mode_key).equals(key)) {
updateFlags |= LIST_MODE_UPDATE_FLAG;
}
}

View file

@ -12,14 +12,19 @@ import androidx.recyclerview.widget.RecyclerView;
import org.schabi.newpipe.database.LocalItem;
import org.schabi.newpipe.database.stream.model.StreamStateEntity;
import org.schabi.newpipe.info_list.ItemViewMode;
import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.local.holder.LocalItemHolder;
import org.schabi.newpipe.local.holder.LocalPlaylistCardItemHolder;
import org.schabi.newpipe.local.holder.LocalPlaylistGridItemHolder;
import org.schabi.newpipe.local.holder.LocalPlaylistItemHolder;
import org.schabi.newpipe.local.holder.LocalPlaylistStreamCardItemHolder;
import org.schabi.newpipe.local.holder.LocalPlaylistStreamGridItemHolder;
import org.schabi.newpipe.local.holder.LocalPlaylistStreamItemHolder;
import org.schabi.newpipe.local.holder.LocalStatisticStreamCardItemHolder;
import org.schabi.newpipe.local.holder.LocalStatisticStreamGridItemHolder;
import org.schabi.newpipe.local.holder.LocalStatisticStreamItemHolder;
import org.schabi.newpipe.local.holder.RemotePlaylistCardItemHolder;
import org.schabi.newpipe.local.holder.RemotePlaylistGridItemHolder;
import org.schabi.newpipe.local.holder.RemotePlaylistItemHolder;
import org.schabi.newpipe.util.FallbackViewHolder;
@ -61,11 +66,17 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
private static final int STREAM_STATISTICS_HOLDER_TYPE = 0x1000;
private static final int STREAM_PLAYLIST_HOLDER_TYPE = 0x1001;
private static final int STREAM_STATISTICS_GRID_HOLDER_TYPE = 0x1002;
private static final int STREAM_STATISTICS_CARD_HOLDER_TYPE = 0x1003;
private static final int STREAM_PLAYLIST_GRID_HOLDER_TYPE = 0x1004;
private static final int STREAM_PLAYLIST_CARD_HOLDER_TYPE = 0x1005;
private static final int LOCAL_PLAYLIST_HOLDER_TYPE = 0x2000;
private static final int REMOTE_PLAYLIST_HOLDER_TYPE = 0x2001;
private static final int LOCAL_PLAYLIST_GRID_HOLDER_TYPE = 0x2002;
private static final int REMOTE_PLAYLIST_GRID_HOLDER_TYPE = 0x2004;
private static final int LOCAL_PLAYLIST_GRID_HOLDER_TYPE = 0x2001;
private static final int LOCAL_PLAYLIST_CARD_HOLDER_TYPE = 0x2002;
private static final int REMOTE_PLAYLIST_HOLDER_TYPE = 0x3000;
private static final int REMOTE_PLAYLIST_GRID_HOLDER_TYPE = 0x3001;
private static final int REMOTE_PLAYLIST_CARD_HOLDER_TYPE = 0x3002;
private final LocalItemBuilder localItemBuilder;
private final ArrayList<LocalItem> localItems;
@ -73,9 +84,9 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
private final DateTimeFormatter dateTimeFormatter;
private boolean showFooter = false;
private boolean useGridVariant = false;
private View header = null;
private View footer = null;
private ItemViewMode itemViewMode = ItemViewMode.LIST;
public LocalItemListAdapter(final Context context) {
recordManager = new HistoryRecordManager(context);
@ -165,8 +176,8 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
notifyDataSetChanged();
}
public void setUseGridVariant(final boolean useGridVariant) {
this.useGridVariant = useGridVariant;
public void setItemViewMode(final ItemViewMode itemViewMode) {
this.itemViewMode = itemViewMode;
}
public void setHeader(final View header) {
@ -244,21 +255,39 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
return FOOTER_TYPE;
}
final LocalItem item = localItems.get(position);
switch (item.getLocalItemType()) {
case PLAYLIST_LOCAL_ITEM:
return useGridVariant
? LOCAL_PLAYLIST_GRID_HOLDER_TYPE : LOCAL_PLAYLIST_HOLDER_TYPE;
if (itemViewMode == ItemViewMode.CARD) {
return LOCAL_PLAYLIST_CARD_HOLDER_TYPE;
} else if (itemViewMode == ItemViewMode.GRID) {
return LOCAL_PLAYLIST_GRID_HOLDER_TYPE;
} else {
return LOCAL_PLAYLIST_HOLDER_TYPE;
}
case PLAYLIST_REMOTE_ITEM:
return useGridVariant
? REMOTE_PLAYLIST_GRID_HOLDER_TYPE : REMOTE_PLAYLIST_HOLDER_TYPE;
if (itemViewMode == ItemViewMode.CARD) {
return REMOTE_PLAYLIST_CARD_HOLDER_TYPE;
} else if (itemViewMode == ItemViewMode.GRID) {
return REMOTE_PLAYLIST_GRID_HOLDER_TYPE;
} else {
return REMOTE_PLAYLIST_HOLDER_TYPE;
}
case PLAYLIST_STREAM_ITEM:
return useGridVariant
? STREAM_PLAYLIST_GRID_HOLDER_TYPE : STREAM_PLAYLIST_HOLDER_TYPE;
if (itemViewMode == ItemViewMode.CARD) {
return STREAM_PLAYLIST_CARD_HOLDER_TYPE;
} else if (itemViewMode == ItemViewMode.GRID) {
return STREAM_PLAYLIST_GRID_HOLDER_TYPE;
} else {
return STREAM_PLAYLIST_HOLDER_TYPE;
}
case STATISTIC_STREAM_ITEM:
return useGridVariant
? STREAM_STATISTICS_GRID_HOLDER_TYPE : STREAM_STATISTICS_HOLDER_TYPE;
if (itemViewMode == ItemViewMode.CARD) {
return STREAM_STATISTICS_CARD_HOLDER_TYPE;
} else if (itemViewMode == ItemViewMode.GRID) {
return STREAM_STATISTICS_GRID_HOLDER_TYPE;
} else {
return STREAM_STATISTICS_HOLDER_TYPE;
}
default:
Log.e(TAG, "No holder type has been considered for item: ["
+ item.getLocalItemType() + "]");
@ -283,18 +312,26 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
return new LocalPlaylistItemHolder(localItemBuilder, parent);
case LOCAL_PLAYLIST_GRID_HOLDER_TYPE:
return new LocalPlaylistGridItemHolder(localItemBuilder, parent);
case LOCAL_PLAYLIST_CARD_HOLDER_TYPE:
return new LocalPlaylistCardItemHolder(localItemBuilder, parent);
case REMOTE_PLAYLIST_HOLDER_TYPE:
return new RemotePlaylistItemHolder(localItemBuilder, parent);
case REMOTE_PLAYLIST_GRID_HOLDER_TYPE:
return new RemotePlaylistGridItemHolder(localItemBuilder, parent);
case REMOTE_PLAYLIST_CARD_HOLDER_TYPE:
return new RemotePlaylistCardItemHolder(localItemBuilder, parent);
case STREAM_PLAYLIST_HOLDER_TYPE:
return new LocalPlaylistStreamItemHolder(localItemBuilder, parent);
case STREAM_PLAYLIST_GRID_HOLDER_TYPE:
return new LocalPlaylistStreamGridItemHolder(localItemBuilder, parent);
case STREAM_PLAYLIST_CARD_HOLDER_TYPE:
return new LocalPlaylistStreamCardItemHolder(localItemBuilder, parent);
case STREAM_STATISTICS_HOLDER_TYPE:
return new LocalStatisticStreamItemHolder(localItemBuilder, parent);
case STREAM_STATISTICS_GRID_HOLDER_TYPE:
return new LocalStatisticStreamGridItemHolder(localItemBuilder, parent);
case STREAM_STATISTICS_CARD_HOLDER_TYPE:
return new LocalStatisticStreamCardItemHolder(localItemBuilder, parent);
default:
Log.e(TAG, "No view type has been considered for holder: [" + type + "]");
return new FallbackViewHolder(new View(parent.getContext()));

View file

@ -1,5 +1,6 @@
package org.schabi.newpipe.local.bookmark;
import android.content.DialogInterface;
import android.os.Bundle;
import android.os.Parcelable;
import android.text.InputType;
@ -31,6 +32,7 @@ import org.schabi.newpipe.local.playlist.RemotePlaylistManager;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.OnClickGesture;
import java.util.ArrayList;
import java.util.List;
import icepick.State;
@ -256,6 +258,41 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
}
private void showLocalDialog(final PlaylistMetadataEntry selectedItem) {
final String rename = getString(R.string.rename);
final String delete = getString(R.string.delete);
final String unsetThumbnail = getString(R.string.unset_playlist_thumbnail);
final boolean isThumbnailPermanent = localPlaylistManager
.getIsPlaylistThumbnailPermanent(selectedItem.uid);
final AlertDialog.Builder builder = new AlertDialog.Builder(activity);
final ArrayList<String> items = new ArrayList<>();
items.add(rename);
items.add(delete);
if (isThumbnailPermanent) {
items.add(unsetThumbnail);
}
final DialogInterface.OnClickListener action = (d, index) -> {
if (items.get(index).equals(rename)) {
showRenameDialog(selectedItem);
} else if (items.get(index).equals(delete)) {
showDeleteDialog(selectedItem.name,
localPlaylistManager.deletePlaylist(selectedItem.uid));
} else if (isThumbnailPermanent && items.get(index).equals(unsetThumbnail)) {
final String thumbnailUrl = localPlaylistManager
.getAutomaticPlaylistThumbnail(selectedItem.uid);
localPlaylistManager
.changePlaylistThumbnail(selectedItem.uid, thumbnailUrl, false)
.observeOn(AndroidSchedulers.mainThread())
.subscribe();
}
};
builder.setItems(items.toArray(new String[0]), action).create().show();
}
private void showRenameDialog(final PlaylistMetadataEntry selectedItem) {
final DialogEditTextBinding dialogBinding =
DialogEditTextBinding.inflate(getLayoutInflater());
dialogBinding.dialogEditText.setHint(R.string.name);
@ -269,11 +306,6 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
selectedItem.uid,
dialogBinding.dialogEditText.getText().toString()))
.setNegativeButton(R.string.cancel, null)
.setNeutralButton(R.string.delete, (dialog, which) -> {
showDeleteDialog(selectedItem.name,
localPlaylistManager.deletePlaylist(selectedItem.uid));
dialog.dismiss();
})
.create()
.show();
}

View file

@ -4,6 +4,7 @@ import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
@ -13,7 +14,7 @@ import androidx.recyclerview.widget.RecyclerView;
import org.schabi.newpipe.NewPipeDatabase;
import org.schabi.newpipe.R;
import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry;
import org.schabi.newpipe.database.playlist.PlaylistDuplicatesEntry;
import org.schabi.newpipe.database.stream.model.StreamEntity;
import org.schabi.newpipe.local.LocalItemListAdapter;
import org.schabi.newpipe.local.playlist.LocalPlaylistManager;
@ -28,6 +29,7 @@ public final class PlaylistAppendDialog extends PlaylistDialog {
private RecyclerView playlistRecyclerView;
private LocalItemListAdapter playlistAdapter;
private TextView playlistDuplicateIndicator;
private final CompositeDisposable playlistDisposables = new CompositeDisposable();
@ -63,8 +65,9 @@ public final class PlaylistAppendDialog extends PlaylistDialog {
playlistAdapter = new LocalItemListAdapter(getActivity());
playlistAdapter.setSelectedListener(selectedItem -> {
final List<StreamEntity> entities = getStreamEntities();
if (selectedItem instanceof PlaylistMetadataEntry && entities != null) {
onPlaylistSelected(playlistManager, (PlaylistMetadataEntry) selectedItem, entities);
if (selectedItem instanceof PlaylistDuplicatesEntry && entities != null) {
onPlaylistSelected(playlistManager,
(PlaylistDuplicatesEntry) selectedItem, entities);
}
});
@ -72,10 +75,13 @@ public final class PlaylistAppendDialog extends PlaylistDialog {
playlistRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext()));
playlistRecyclerView.setAdapter(playlistAdapter);
playlistDuplicateIndicator = view.findViewById(R.id.playlist_duplicate);
final View newPlaylistButton = view.findViewById(R.id.newPlaylist);
newPlaylistButton.setOnClickListener(ignored -> openCreatePlaylistDialog());
playlistDisposables.add(playlistManager.getPlaylists()
playlistDisposables.add(playlistManager
.getPlaylistDuplicates(getStreamEntities().get(0).getUrl())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(this::onPlaylistsReceived));
}
@ -117,24 +123,41 @@ public final class PlaylistAppendDialog extends PlaylistDialog {
requireDialog().dismiss();
}
private void onPlaylistsReceived(@NonNull final List<PlaylistMetadataEntry> playlists) {
if (playlistAdapter != null && playlistRecyclerView != null) {
private void onPlaylistsReceived(@NonNull final List<PlaylistDuplicatesEntry> playlists) {
if (playlistAdapter != null
&& playlistRecyclerView != null
&& playlistDuplicateIndicator != null) {
playlistAdapter.clearStreamItemList();
playlistAdapter.addItems(playlists);
playlistRecyclerView.setVisibility(View.VISIBLE);
playlistDuplicateIndicator.setVisibility(
anyPlaylistContainsDuplicates(playlists) ? View.VISIBLE : View.GONE);
}
}
private boolean anyPlaylistContainsDuplicates(final List<PlaylistDuplicatesEntry> playlists) {
return playlists.stream()
.anyMatch(playlist -> playlist.timesStreamIsContained > 0);
}
private void onPlaylistSelected(@NonNull final LocalPlaylistManager manager,
@NonNull final PlaylistMetadataEntry playlist,
@NonNull final PlaylistDuplicatesEntry playlist,
@NonNull final List<StreamEntity> streams) {
final Toast successToast = Toast.makeText(getContext(),
R.string.playlist_add_stream_success, Toast.LENGTH_SHORT);
final String toastText;
if (playlist.timesStreamIsContained > 0) {
toastText = getString(R.string.playlist_add_stream_success_duplicate,
playlist.timesStreamIsContained);
} else {
toastText = getString(R.string.playlist_add_stream_success);
}
final Toast successToast = Toast.makeText(getContext(), toastText, Toast.LENGTH_SHORT);
if (playlist.thumbnailUrl
.equals("drawable://" + R.drawable.placeholder_thumbnail_playlist)) {
playlistDisposables.add(manager
.changePlaylistThumbnail(playlist.uid, streams.get(0).getThumbnailUrl())
.changePlaylistThumbnail(playlist.uid, streams.get(0).getThumbnailUrl(), false)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(ignored -> successToast.show()));
}

View file

@ -36,7 +36,6 @@ import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import androidx.annotation.Nullable
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.content.res.AppCompatResources
import androidx.core.content.edit
@ -69,6 +68,7 @@ import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException
import org.schabi.newpipe.extractor.stream.StreamInfoItem
import org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty
import org.schabi.newpipe.fragments.BaseStateFragment
import org.schabi.newpipe.info_list.ItemViewMode
import org.schabi.newpipe.info_list.dialog.InfoItemDialog
import org.schabi.newpipe.ktx.animate
import org.schabi.newpipe.ktx.animateHideRecyclerViewAllowingScrolling
@ -80,6 +80,7 @@ import org.schabi.newpipe.util.DeviceUtils
import org.schabi.newpipe.util.Localization
import org.schabi.newpipe.util.NavigationHelper
import org.schabi.newpipe.util.ThemeHelper.getGridSpanCountStreams
import org.schabi.newpipe.util.ThemeHelper.getItemViewMode
import org.schabi.newpipe.util.ThemeHelper.resolveDrawable
import org.schabi.newpipe.util.ThemeHelper.shouldUseGridLayout
import java.time.OffsetDateTime
@ -120,7 +121,7 @@ class FeedFragment : BaseStateFragment<FeedState>() {
groupName = arguments?.getString(KEY_GROUP_NAME) ?: ""
onSettingsChangeListener = SharedPreferences.OnSharedPreferenceChangeListener { _, key ->
if (key.equals(getString(R.string.list_view_mode_key))) {
if (getString(R.string.list_view_mode_key).equals(key)) {
updateListViewModeOnResume = true
}
}
@ -416,11 +417,10 @@ class FeedFragment : BaseStateFragment<FeedState>() {
@SuppressLint("StringFormatMatches")
private fun handleLoadedState(loadedState: FeedState.LoadedState) {
val itemVersion = if (shouldUseGridLayout(context)) {
StreamItem.ItemVersion.GRID
} else {
StreamItem.ItemVersion.NORMAL
val itemVersion = when (getItemViewMode(requireContext())) {
ItemViewMode.GRID -> StreamItem.ItemVersion.GRID
ItemViewMode.CARD -> StreamItem.ItemVersion.CARD
else -> StreamItem.ItemVersion.NORMAL
}
loadedState.items.forEach { it.itemVersion = itemVersion }
@ -499,7 +499,7 @@ class FeedFragment : BaseStateFragment<FeedState>() {
private fun handleFeedNotAvailable(
subscriptionEntity: SubscriptionEntity,
@Nullable cause: Throwable?,
cause: Throwable?,
nextItemsErrors: List<Throwable>
) {
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext())

View file

@ -42,12 +42,13 @@ data class StreamItem(
override fun getId(): Long = stream.uid
enum class ItemVersion { NORMAL, MINI, GRID }
enum class ItemVersion { NORMAL, MINI, GRID, CARD }
override fun getLayout(): Int = when (itemVersion) {
ItemVersion.NORMAL -> R.layout.list_stream_item
ItemVersion.MINI -> R.layout.list_stream_mini_item
ItemVersion.GRID -> R.layout.list_stream_grid_item
ItemVersion.CARD -> R.layout.list_stream_card_item
}
override fun initializeViewBinding(view: View) = ListStreamItemBinding.bind(view)

View file

@ -0,0 +1,17 @@
package org.schabi.newpipe.local.holder;
import android.view.ViewGroup;
import org.schabi.newpipe.R;
import org.schabi.newpipe.local.LocalItemBuilder;
/**
* Playlist card layout.
*/
public class LocalPlaylistCardItemHolder extends LocalPlaylistItemHolder {
public LocalPlaylistCardItemHolder(final LocalItemBuilder infoItemBuilder,
final ViewGroup parent) {
super(infoItemBuilder, R.layout.list_playlist_card_item, parent);
}
}

View file

@ -4,6 +4,7 @@ import android.view.View;
import android.view.ViewGroup;
import org.schabi.newpipe.database.LocalItem;
import org.schabi.newpipe.database.playlist.PlaylistDuplicatesEntry;
import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry;
import org.schabi.newpipe.local.LocalItemBuilder;
import org.schabi.newpipe.local.history.HistoryRecordManager;
@ -13,6 +14,9 @@ import org.schabi.newpipe.util.Localization;
import java.time.format.DateTimeFormatter;
public class LocalPlaylistItemHolder extends PlaylistItemHolder {
private static final float GRAYED_OUT_ALPHA = 0.6f;
public LocalPlaylistItemHolder(final LocalItemBuilder infoItemBuilder, final ViewGroup parent) {
super(infoItemBuilder, parent);
}
@ -38,6 +42,13 @@ public class LocalPlaylistItemHolder extends PlaylistItemHolder {
PicassoHelper.loadPlaylistThumbnail(item.thumbnailUrl).into(itemThumbnailView);
if (item instanceof PlaylistDuplicatesEntry
&& ((PlaylistDuplicatesEntry) item).timesStreamIsContained > 0) {
itemView.setAlpha(GRAYED_OUT_ALPHA);
} else {
itemView.setAlpha(1.0f);
}
super.updateFromItem(localItem, historyRecordManager, dateTimeFormatter);
}
}

View file

@ -0,0 +1,17 @@
package org.schabi.newpipe.local.holder;
import android.view.ViewGroup;
import org.schabi.newpipe.R;
import org.schabi.newpipe.local.LocalItemBuilder;
/**
* Local playlist stream UI. This also includes a handle to rearrange the videos.
*/
public class LocalPlaylistStreamCardItemHolder extends LocalPlaylistStreamItemHolder {
public LocalPlaylistStreamCardItemHolder(final LocalItemBuilder infoItemBuilder,
final ViewGroup parent) {
super(infoItemBuilder, R.layout.list_stream_playlist_card_item, parent);
}
}

View file

@ -0,0 +1,13 @@
package org.schabi.newpipe.local.holder;
import android.view.ViewGroup;
import org.schabi.newpipe.R;
import org.schabi.newpipe.local.LocalItemBuilder;
public class LocalStatisticStreamCardItemHolder extends LocalStatisticStreamItemHolder {
public LocalStatisticStreamCardItemHolder(final LocalItemBuilder infoItemBuilder,
final ViewGroup parent) {
super(infoItemBuilder, R.layout.list_stream_card_item, parent);
}
}

View file

@ -0,0 +1,17 @@
package org.schabi.newpipe.local.holder;
import android.view.ViewGroup;
import org.schabi.newpipe.R;
import org.schabi.newpipe.local.LocalItemBuilder;
/**
* Playlist card UI for list item.
*/
public class RemotePlaylistCardItemHolder extends RemotePlaylistItemHolder {
public RemotePlaylistCardItemHolder(final LocalItemBuilder infoItemBuilder,
final ViewGroup parent) {
super(infoItemBuilder, R.layout.list_playlist_card_item, parent);
}
}

View file

@ -408,7 +408,9 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
.firstElement()
.zipWith(historyIdsMaybe, (playlist, historyStreamIds) -> {
// Remove Watched, Functionality data
final List<PlaylistStreamEntry> notWatchedItems = new ArrayList<>();
final List<PlaylistStreamEntry> itemsToKeep = new ArrayList<>();
final boolean isThumbnailPermanent = playlistManager
.getIsPlaylistThumbnailPermanent(playlistId);
boolean thumbnailVideoRemoved = false;
if (removePartiallyWatched) {
@ -417,8 +419,8 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
playlistItem.getStreamId());
if (indexInHistory < 0) {
notWatchedItems.add(playlistItem);
} else if (!thumbnailVideoRemoved
itemsToKeep.add(playlistItem);
} else if (!isThumbnailPermanent && !thumbnailVideoRemoved
&& playlistManager.getPlaylistThumbnail(playlistId)
.equals(playlistItem.getStreamEntity().getThumbnailUrl())) {
thumbnailVideoRemoved = true;
@ -438,8 +440,8 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
if (indexInHistory < 0 || (streamStateEntity != null
&& !streamStateEntity.isFinished(duration))) {
notWatchedItems.add(playlistItem);
} else if (!thumbnailVideoRemoved
itemsToKeep.add(playlistItem);
} else if (!isThumbnailPermanent && !thumbnailVideoRemoved
&& playlistManager.getPlaylistThumbnail(playlistId)
.equals(playlistItem.getStreamEntity().getThumbnailUrl())) {
thumbnailVideoRemoved = true;
@ -447,17 +449,17 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
}
}
return new Pair<>(notWatchedItems, thumbnailVideoRemoved);
return new Pair<>(itemsToKeep, thumbnailVideoRemoved);
});
disposables.add(streamsMaybe.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(flow -> {
final List<PlaylistStreamEntry> notWatchedItems = flow.first;
final List<PlaylistStreamEntry> itemsToKeep = flow.first;
final boolean thumbnailVideoRemoved = flow.second;
itemListAdapter.clearStreamItemList();
itemListAdapter.addItems(notWatchedItems);
itemListAdapter.addItems(itemsToKeep);
saveChanges();
if (thumbnailVideoRemoved) {
@ -589,8 +591,9 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
disposables.add(disposable);
}
private void changeThumbnailUrl(final String thumbnailUrl) {
if (playlistManager == null) {
private void changeThumbnailUrl(final String thumbnailUrl, final boolean isPermanent) {
if (playlistManager == null || (!isPermanent && playlistManager
.getIsPlaylistThumbnailPermanent(playlistId))) {
return;
}
@ -604,7 +607,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
}
final Disposable disposable = playlistManager
.changePlaylistThumbnail(playlistId, thumbnailUrl)
.changePlaylistThumbnail(playlistId, thumbnailUrl, isPermanent)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(ignore -> successToast.show(), throwable ->
showError(new ErrorInfo(throwable, UserAction.REQUESTED_BOOKMARK,
@ -613,6 +616,10 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
}
private void updateThumbnailUrl() {
if (playlistManager.getIsPlaylistThumbnailPermanent(playlistId)) {
return;
}
final String newThumbnailUrl;
if (!itemListAdapter.getItemsList().isEmpty()) {
@ -622,7 +629,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
newThumbnailUrl = "drawable://" + R.drawable.placeholder_thumbnail_playlist;
}
changeThumbnailUrl(newThumbnailUrl);
changeThumbnailUrl(newThumbnailUrl, false);
}
private void openRemoveDuplicatesDialog() {
@ -828,7 +835,8 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
.setAction(
StreamDialogDefaultEntry.SET_AS_PLAYLIST_THUMBNAIL,
(f, i) ->
changeThumbnailUrl(item.getStreamEntity().getThumbnailUrl()))
changeThumbnailUrl(item.getStreamEntity().getThumbnailUrl(),
true))
.setAction(
StreamDialogDefaultEntry.DELETE,
(f, i) -> deleteItem(item))

View file

@ -2,7 +2,9 @@ package org.schabi.newpipe.local.playlist;
import androidx.annotation.Nullable;
import org.schabi.newpipe.R;
import org.schabi.newpipe.database.AppDatabase;
import org.schabi.newpipe.database.playlist.PlaylistDuplicatesEntry;
import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry;
import org.schabi.newpipe.database.playlist.PlaylistStreamEntry;
import org.schabi.newpipe.database.playlist.dao.PlaylistDAO;
@ -41,7 +43,7 @@ public class LocalPlaylistManager {
}
final StreamEntity defaultStream = streams.get(0);
final PlaylistEntity newPlaylist =
new PlaylistEntity(name, defaultStream.getThumbnailUrl());
new PlaylistEntity(name, defaultStream.getThumbnailUrl(), false);
return Maybe.fromCallable(() -> database.runInTransaction(() ->
upsertStreams(playlistTable.insert(newPlaylist), streams, 0))
@ -91,6 +93,18 @@ public class LocalPlaylistManager {
.getStreamsWithoutDuplicates(playlistId).subscribeOn(Schedulers.io());
}
/**
* Get playlists with attached information about how many times the provided stream is already
* contained in each playlist.
*
* @param streamUrl the stream url for which to check for duplicates
* @return a list of {@link PlaylistDuplicatesEntry}
*/
public Flowable<List<PlaylistDuplicatesEntry>> getPlaylistDuplicates(final String streamUrl) {
return playlistStreamTable.getPlaylistDuplicatesMetadata(streamUrl)
.subscribeOn(Schedulers.io());
}
public Flowable<List<PlaylistStreamEntry>> getPlaylistStreams(final long playlistId) {
return playlistStreamTable.getOrderedStreamsOf(playlistId).subscribeOn(Schedulers.io());
}
@ -101,21 +115,33 @@ public class LocalPlaylistManager {
}
public Maybe<Integer> renamePlaylist(final long playlistId, final String name) {
return modifyPlaylist(playlistId, name, null);
return modifyPlaylist(playlistId, name, null, false);
}
public Maybe<Integer> changePlaylistThumbnail(final long playlistId,
final String thumbnailUrl) {
return modifyPlaylist(playlistId, null, thumbnailUrl);
final String thumbnailUrl,
final boolean isPermanent) {
return modifyPlaylist(playlistId, null, thumbnailUrl, isPermanent);
}
public String getPlaylistThumbnail(final long playlistId) {
return playlistTable.getPlaylist(playlistId).blockingFirst().get(0).getThumbnailUrl();
}
public boolean getIsPlaylistThumbnailPermanent(final long playlistId) {
return playlistTable.getPlaylist(playlistId).blockingFirst().get(0)
.getIsThumbnailPermanent();
}
public String getAutomaticPlaylistThumbnail(final long playlistId) {
final String def = "drawable://" + R.drawable.placeholder_thumbnail_playlist;
return playlistStreamTable.getAutomaticThumbnailUrl(playlistId, def).blockingFirst();
}
private Maybe<Integer> modifyPlaylist(final long playlistId,
@Nullable final String name,
@Nullable final String thumbnailUrl) {
@Nullable final String thumbnailUrl,
final boolean isPermanent) {
return playlistTable.getPlaylist(playlistId)
.firstElement()
.filter(playlistEntities -> !playlistEntities.isEmpty())
@ -126,6 +152,7 @@ public class LocalPlaylistManager {
}
if (thumbnailUrl != null) {
playlist.setThumbnailUrl(thumbnailUrl);
playlist.setIsThumbnailPermanent(isPermanent);
}
return playlistTable.update(playlist);
}).subscribeOn(Schedulers.io());

View file

@ -51,7 +51,8 @@ enum class FeedGroupIcon(
WORLD(34, R.drawable.ic_public),
STAR(35, R.drawable.ic_stars),
SUN(36, R.drawable.ic_wb_sunny),
RSS(37, R.drawable.ic_rss_feed);
RSS(37, R.drawable.ic_rss_feed),
WHATS_NEW(38, R.drawable.ic_subscriptions);
@DrawableRes
fun getDrawableRes(): Int {

View file

@ -433,10 +433,10 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
clear()
if (listViewMode) {
add(FeedGroupAddNewItem())
add(FeedGroupCardItem(GROUP_ALL_ID, getString(R.string.all), FeedGroupIcon.RSS))
add(FeedGroupCardItem(GROUP_ALL_ID, getString(R.string.all), FeedGroupIcon.WHATS_NEW))
} else {
add(FeedGroupAddNewGridItem())
add(FeedGroupCardGridItem(GROUP_ALL_ID, getString(R.string.all), FeedGroupIcon.RSS))
add(FeedGroupCardGridItem(GROUP_ALL_ID, getString(R.string.all), FeedGroupIcon.WHATS_NEW))
}
addAll(groups)
}