Thumbnails used in NewPipe are small (list/grid) mode. This PR facilitates full width thumbnails and dubbed as card mode.

This commit is contained in:
Mahendran 2022-11-03 00:26:10 +05:30
parent c47d1af5e3
commit 7924bb5b6b
24 changed files with 601 additions and 69 deletions

View file

@ -26,6 +26,7 @@ import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.fragments.BaseStateFragment; import org.schabi.newpipe.fragments.BaseStateFragment;
import org.schabi.newpipe.fragments.OnScrollBelowItemsListener; import org.schabi.newpipe.fragments.OnScrollBelowItemsListener;
import org.schabi.newpipe.info_list.InfoListAdapter; import org.schabi.newpipe.info_list.InfoListAdapter;
import org.schabi.newpipe.info_list.ItemViewMode;
import org.schabi.newpipe.info_list.dialog.InfoItemDialog; import org.schabi.newpipe.info_list.dialog.InfoItemDialog;
import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.OnClickGesture; import org.schabi.newpipe.util.OnClickGesture;
@ -91,11 +92,7 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I>
if (updateFlags != 0) { if (updateFlags != 0) {
if ((updateFlags & LIST_MODE_UPDATE_FLAG) != 0) { if ((updateFlags & LIST_MODE_UPDATE_FLAG) != 0) {
final boolean useGrid = isGridLayout(); refreshItemViewMode();
itemsList.setLayoutManager(useGrid
? getGridLayoutManager() : getListLayoutManager());
infoListAdapter.setUseGridVariant(useGrid);
infoListAdapter.notifyDataSetChanged();
} }
updateFlags = 0; updateFlags = 0;
} }
@ -221,15 +218,23 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I>
return lm; return lm;
} }
/**
* Updates the item view mode based on user preference.
*/
private void refreshItemViewMode() {
final ItemViewMode itemViewMode = getItemViewMode();
itemsList.setLayoutManager((itemViewMode == ItemViewMode.GRID)
? getGridLayoutManager() : getListLayoutManager());
infoListAdapter.setItemViewMode(itemViewMode);
infoListAdapter.notifyDataSetChanged();
}
@Override @Override
protected void initViews(final View rootView, final Bundle savedInstanceState) { protected void initViews(final View rootView, final Bundle savedInstanceState) {
super.initViews(rootView, savedInstanceState); super.initViews(rootView, savedInstanceState);
final boolean useGrid = isGridLayout();
itemsList = rootView.findViewById(R.id.items_list); itemsList = rootView.findViewById(R.id.items_list);
itemsList.setLayoutManager(useGrid ? getGridLayoutManager() : getListLayoutManager()); refreshItemViewMode();
infoListAdapter.setUseGridVariant(useGrid);
final Supplier<View> listHeaderSupplier = getListHeaderSupplier(); final Supplier<View> listHeaderSupplier = getListHeaderSupplier();
if (listHeaderSupplier != null) { if (listHeaderSupplier != null) {
@ -474,7 +479,11 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I>
} }
} }
protected boolean isGridLayout() { /**
return ThemeHelper.shouldUseGridLayout(activity); * Returns preferred item view mode.
* @return ItemViewMode
*/
protected ItemViewMode getItemViewMode() {
return ThemeHelper.getItemViewMode(requireContext());
} }
} }

View file

@ -17,6 +17,7 @@ import org.schabi.newpipe.extractor.ListExtractor;
import org.schabi.newpipe.extractor.comments.CommentsInfo; import org.schabi.newpipe.extractor.comments.CommentsInfo;
import org.schabi.newpipe.extractor.comments.CommentsInfoItem; import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
import org.schabi.newpipe.fragments.list.BaseListInfoFragment; import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
import org.schabi.newpipe.info_list.ItemViewMode;
import org.schabi.newpipe.ktx.ViewUtils; import org.schabi.newpipe.ktx.ViewUtils;
import org.schabi.newpipe.util.ExtractorHelper; import org.schabi.newpipe.util.ExtractorHelper;
@ -106,7 +107,7 @@ public class CommentsFragment extends BaseListInfoFragment<CommentsInfoItem, Com
@NonNull final MenuInflater inflater) { } @NonNull final MenuInflater inflater) { }
@Override @Override
protected boolean isGridLayout() { protected ItemViewMode getItemViewMode() {
return false; return ItemViewMode.LIST;
} }
} }

View file

@ -132,6 +132,8 @@ public class PlaylistFragment extends BaseListInfoFragment<StreamInfoItem, Playl
protected void initViews(final View rootView, final Bundle savedInstanceState) { protected void initViews(final View rootView, final Bundle savedInstanceState) {
super.initViews(rootView, savedInstanceState); super.initViews(rootView, savedInstanceState);
// Is mini variant still relevant?
// Only the remote playlist screen uses it now
infoListAdapter.setUseMiniVariant(true); infoListAdapter.setUseMiniVariant(true);
} }

View file

@ -19,6 +19,7 @@ import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.ListExtractor; import org.schabi.newpipe.extractor.ListExtractor;
import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.fragments.list.BaseListInfoFragment; import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
import org.schabi.newpipe.info_list.ItemViewMode;
import org.schabi.newpipe.ktx.ViewUtils; import org.schabi.newpipe.ktx.ViewUtils;
import org.schabi.newpipe.util.RelatedItemInfo; import org.schabi.newpipe.util.RelatedItemInfo;
@ -167,7 +168,12 @@ public class RelatedItemsFragment extends BaseListInfoFragment<InfoItem, Related
} }
@Override @Override
protected boolean isGridLayout() { protected ItemViewMode getItemViewMode() {
return false; ItemViewMode mode = super.getItemViewMode();
// Only list mode is supported. Either List or card will be used.
if (mode != ItemViewMode.LIST && mode != ItemViewMode.CARD) {
mode = ItemViewMode.LIST;
}
return mode;
} }
} }

View file

@ -23,9 +23,11 @@ import org.schabi.newpipe.info_list.holder.ChannelMiniInfoItemHolder;
import org.schabi.newpipe.info_list.holder.CommentsInfoItemHolder; import org.schabi.newpipe.info_list.holder.CommentsInfoItemHolder;
import org.schabi.newpipe.info_list.holder.CommentsMiniInfoItemHolder; import org.schabi.newpipe.info_list.holder.CommentsMiniInfoItemHolder;
import org.schabi.newpipe.info_list.holder.InfoItemHolder; import org.schabi.newpipe.info_list.holder.InfoItemHolder;
import org.schabi.newpipe.info_list.holder.PlaylistCardInfoItemHolder;
import org.schabi.newpipe.info_list.holder.PlaylistGridInfoItemHolder; import org.schabi.newpipe.info_list.holder.PlaylistGridInfoItemHolder;
import org.schabi.newpipe.info_list.holder.PlaylistInfoItemHolder; import org.schabi.newpipe.info_list.holder.PlaylistInfoItemHolder;
import org.schabi.newpipe.info_list.holder.PlaylistMiniInfoItemHolder; import org.schabi.newpipe.info_list.holder.PlaylistMiniInfoItemHolder;
import org.schabi.newpipe.info_list.holder.StreamCardInfoItemHolder;
import org.schabi.newpipe.info_list.holder.StreamGridInfoItemHolder; import org.schabi.newpipe.info_list.holder.StreamGridInfoItemHolder;
import org.schabi.newpipe.info_list.holder.StreamInfoItemHolder; import org.schabi.newpipe.info_list.holder.StreamInfoItemHolder;
import org.schabi.newpipe.info_list.holder.StreamMiniInfoItemHolder; import org.schabi.newpipe.info_list.holder.StreamMiniInfoItemHolder;
@ -67,12 +69,14 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
private static final int MINI_STREAM_HOLDER_TYPE = 0x100; private static final int MINI_STREAM_HOLDER_TYPE = 0x100;
private static final int STREAM_HOLDER_TYPE = 0x101; private static final int STREAM_HOLDER_TYPE = 0x101;
private static final int GRID_STREAM_HOLDER_TYPE = 0x102; private static final int GRID_STREAM_HOLDER_TYPE = 0x102;
private static final int CARD_STREAM_HOLDER_TYPE = 0x103;
private static final int MINI_CHANNEL_HOLDER_TYPE = 0x200; private static final int MINI_CHANNEL_HOLDER_TYPE = 0x200;
private static final int CHANNEL_HOLDER_TYPE = 0x201; private static final int CHANNEL_HOLDER_TYPE = 0x201;
private static final int GRID_CHANNEL_HOLDER_TYPE = 0x202; private static final int GRID_CHANNEL_HOLDER_TYPE = 0x202;
private static final int MINI_PLAYLIST_HOLDER_TYPE = 0x300; private static final int MINI_PLAYLIST_HOLDER_TYPE = 0x300;
private static final int PLAYLIST_HOLDER_TYPE = 0x301; private static final int PLAYLIST_HOLDER_TYPE = 0x301;
private static final int GRID_PLAYLIST_HOLDER_TYPE = 0x302; private static final int GRID_PLAYLIST_HOLDER_TYPE = 0x302;
private static final int CARD_PLAYLIST_HOLDER_TYPE = 0x303;
private static final int MINI_COMMENT_HOLDER_TYPE = 0x400; private static final int MINI_COMMENT_HOLDER_TYPE = 0x400;
private static final int COMMENT_HOLDER_TYPE = 0x401; private static final int COMMENT_HOLDER_TYPE = 0x401;
@ -82,9 +86,10 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
private final HistoryRecordManager recordManager; private final HistoryRecordManager recordManager;
private boolean useMiniVariant = false; private boolean useMiniVariant = false;
private boolean useGridVariant = false;
private boolean showFooter = false; private boolean showFooter = false;
private ItemViewMode itemMode = ItemViewMode.LIST;
private Supplier<View> headerSupplier = null; private Supplier<View> headerSupplier = null;
public InfoListAdapter(final Context context) { public InfoListAdapter(final Context context) {
@ -114,8 +119,8 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
this.useMiniVariant = useMiniVariant; this.useMiniVariant = useMiniVariant;
} }
public void setUseGridVariant(final boolean useGridVariant) { public void setItemViewMode(final ItemViewMode itemViewMode) {
this.useGridVariant = useGridVariant; this.itemMode = itemViewMode;
} }
public void addInfoItemList(@Nullable final List<? extends InfoItem> data) { public void addInfoItemList(@Nullable final List<? extends InfoItem> data) {
@ -234,14 +239,33 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
final InfoItem item = infoItemList.get(position); final InfoItem item = infoItemList.get(position);
switch (item.getInfoType()) { switch (item.getInfoType()) {
case STREAM: case STREAM:
return useGridVariant ? GRID_STREAM_HOLDER_TYPE : useMiniVariant if (itemMode == ItemViewMode.CARD) {
? MINI_STREAM_HOLDER_TYPE : STREAM_HOLDER_TYPE; return CARD_STREAM_HOLDER_TYPE;
} else if (itemMode == ItemViewMode.GRID) {
return GRID_STREAM_HOLDER_TYPE;
} else if (useMiniVariant) {
return MINI_STREAM_HOLDER_TYPE;
} else {
return STREAM_HOLDER_TYPE;
}
case CHANNEL: case CHANNEL:
return useGridVariant ? GRID_CHANNEL_HOLDER_TYPE : useMiniVariant if (itemMode == ItemViewMode.GRID) {
? MINI_CHANNEL_HOLDER_TYPE : CHANNEL_HOLDER_TYPE; return GRID_CHANNEL_HOLDER_TYPE;
} else if (useMiniVariant) {
return MINI_CHANNEL_HOLDER_TYPE;
} else {
return CHANNEL_HOLDER_TYPE;
}
case PLAYLIST: case PLAYLIST:
return useGridVariant ? GRID_PLAYLIST_HOLDER_TYPE : useMiniVariant if (itemMode == ItemViewMode.CARD) {
? MINI_PLAYLIST_HOLDER_TYPE : PLAYLIST_HOLDER_TYPE; return CARD_PLAYLIST_HOLDER_TYPE;
} else if (itemMode == ItemViewMode.GRID) {
return GRID_PLAYLIST_HOLDER_TYPE;
} else if (useMiniVariant) {
return MINI_PLAYLIST_HOLDER_TYPE;
} else {
return PLAYLIST_HOLDER_TYPE;
}
case COMMENT: case COMMENT:
return useMiniVariant ? MINI_COMMENT_HOLDER_TYPE : COMMENT_HOLDER_TYPE; return useMiniVariant ? MINI_COMMENT_HOLDER_TYPE : COMMENT_HOLDER_TYPE;
default: default:
@ -274,6 +298,8 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
return new StreamInfoItemHolder(infoItemBuilder, parent); return new StreamInfoItemHolder(infoItemBuilder, parent);
case GRID_STREAM_HOLDER_TYPE: case GRID_STREAM_HOLDER_TYPE:
return new StreamGridInfoItemHolder(infoItemBuilder, parent); return new StreamGridInfoItemHolder(infoItemBuilder, parent);
case CARD_STREAM_HOLDER_TYPE:
return new StreamCardInfoItemHolder(infoItemBuilder, parent);
case MINI_CHANNEL_HOLDER_TYPE: case MINI_CHANNEL_HOLDER_TYPE:
return new ChannelMiniInfoItemHolder(infoItemBuilder, parent); return new ChannelMiniInfoItemHolder(infoItemBuilder, parent);
case CHANNEL_HOLDER_TYPE: case CHANNEL_HOLDER_TYPE:
@ -286,6 +312,8 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
return new PlaylistInfoItemHolder(infoItemBuilder, parent); return new PlaylistInfoItemHolder(infoItemBuilder, parent);
case GRID_PLAYLIST_HOLDER_TYPE: case GRID_PLAYLIST_HOLDER_TYPE:
return new PlaylistGridInfoItemHolder(infoItemBuilder, parent); return new PlaylistGridInfoItemHolder(infoItemBuilder, parent);
case CARD_PLAYLIST_HOLDER_TYPE:
return new PlaylistCardInfoItemHolder(infoItemBuilder, parent);
case MINI_COMMENT_HOLDER_TYPE: case MINI_COMMENT_HOLDER_TYPE:
return new CommentsMiniInfoItemHolder(infoItemBuilder, parent); return new CommentsMiniInfoItemHolder(infoItemBuilder, parent);
case COMMENT_HOLDER_TYPE: case COMMENT_HOLDER_TYPE:

View file

@ -0,0 +1,23 @@
package org.schabi.newpipe.info_list;
/**
* Item view mode for streams & playlist listing screens.
*/
public enum ItemViewMode {
/**
* Default mode.
*/
AUTO,
/**
* Full width list item with thumb on the left and two line title & uploader in right.
*/
LIST,
/**
* Grid mode places two cards per row.
*/
GRID,
/**
* A full width card in phone - portrait.
*/
CARD
}

View file

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

View file

@ -0,0 +1,16 @@
package org.schabi.newpipe.info_list.holder;
import android.view.ViewGroup;
import org.schabi.newpipe.R;
import org.schabi.newpipe.info_list.InfoItemBuilder;
/**
* Card layout for stream.
*/
public class StreamCardInfoItemHolder extends StreamInfoItemHolder {
public StreamCardInfoItemHolder(final InfoItemBuilder infoItemBuilder, final ViewGroup parent) {
super(infoItemBuilder, R.layout.list_stream_card_item, parent);
}
}

View file

@ -22,10 +22,11 @@ import org.schabi.newpipe.R;
import org.schabi.newpipe.databinding.PignateFooterBinding; import org.schabi.newpipe.databinding.PignateFooterBinding;
import org.schabi.newpipe.fragments.BaseStateFragment; import org.schabi.newpipe.fragments.BaseStateFragment;
import org.schabi.newpipe.fragments.list.ListViewContract; 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.animate;
import static org.schabi.newpipe.ktx.ViewUtils.animateHideRecyclerViewAllowingScrolling; 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 * 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(); super.onResume();
if (updateFlags != 0) { if (updateFlags != 0) {
if ((updateFlags & LIST_MODE_UPDATE_FLAG) != 0) { if ((updateFlags & LIST_MODE_UPDATE_FLAG) != 0) {
final boolean useGrid = shouldUseGridLayout(requireContext()); refreshItemViewMode();
itemsList.setLayoutManager(
useGrid ? getGridLayoutManager() : getListLayoutManager());
itemListAdapter.setUseGridVariant(useGrid);
itemListAdapter.notifyDataSetChanged();
} }
updateFlags = 0; 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 // Lifecycle - View
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
@ -120,11 +128,9 @@ public abstract class BaseLocalListFragment<I, N> extends BaseStateFragment<I>
itemListAdapter = new LocalItemListAdapter(activity); itemListAdapter = new LocalItemListAdapter(activity);
final boolean useGrid = shouldUseGridLayout(requireContext());
itemsList = rootView.findViewById(R.id.items_list); itemsList = rootView.findViewById(R.id.items_list);
itemsList.setLayoutManager(useGrid ? getGridLayoutManager() : getListLayoutManager()); refreshItemViewMode();
itemListAdapter.setUseGridVariant(useGrid);
headerRootBinding = getListHeader(); headerRootBinding = getListHeader();
if (headerRootBinding != null) { if (headerRootBinding != null) {
itemListAdapter.setHeader(headerRootBinding.getRoot()); itemListAdapter.setHeader(headerRootBinding.getRoot());

View file

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

View file

@ -69,6 +69,7 @@ import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException
import org.schabi.newpipe.extractor.stream.StreamInfoItem import org.schabi.newpipe.extractor.stream.StreamInfoItem
import org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty import org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty
import org.schabi.newpipe.fragments.BaseStateFragment 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.info_list.dialog.InfoItemDialog
import org.schabi.newpipe.ktx.animate import org.schabi.newpipe.ktx.animate
import org.schabi.newpipe.ktx.animateHideRecyclerViewAllowingScrolling import org.schabi.newpipe.ktx.animateHideRecyclerViewAllowingScrolling
@ -80,6 +81,7 @@ import org.schabi.newpipe.util.DeviceUtils
import org.schabi.newpipe.util.Localization import org.schabi.newpipe.util.Localization
import org.schabi.newpipe.util.NavigationHelper import org.schabi.newpipe.util.NavigationHelper
import org.schabi.newpipe.util.ThemeHelper.getGridSpanCountStreams 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.resolveDrawable
import org.schabi.newpipe.util.ThemeHelper.shouldUseGridLayout import org.schabi.newpipe.util.ThemeHelper.shouldUseGridLayout
import java.time.OffsetDateTime import java.time.OffsetDateTime
@ -416,11 +418,10 @@ class FeedFragment : BaseStateFragment<FeedState>() {
@SuppressLint("StringFormatMatches") @SuppressLint("StringFormatMatches")
private fun handleLoadedState(loadedState: FeedState.LoadedState) { private fun handleLoadedState(loadedState: FeedState.LoadedState) {
val itemVersion = when (getItemViewMode(requireContext())) {
val itemVersion = if (shouldUseGridLayout(context)) { ItemViewMode.GRID -> StreamItem.ItemVersion.GRID
StreamItem.ItemVersion.GRID ItemViewMode.CARD -> StreamItem.ItemVersion.CARD
} else { else -> StreamItem.ItemVersion.NORMAL
StreamItem.ItemVersion.NORMAL
} }
loadedState.items.forEach { it.itemVersion = itemVersion } loadedState.items.forEach { it.itemVersion = itemVersion }

View file

@ -42,12 +42,13 @@ data class StreamItem(
override fun getId(): Long = stream.uid 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) { override fun getLayout(): Int = when (itemVersion) {
ItemVersion.NORMAL -> R.layout.list_stream_item ItemVersion.NORMAL -> R.layout.list_stream_item
ItemVersion.MINI -> R.layout.list_stream_mini_item ItemVersion.MINI -> R.layout.list_stream_mini_item
ItemVersion.GRID -> R.layout.list_stream_grid_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) 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

@ -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

@ -41,6 +41,7 @@ import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.StreamingService; import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.exceptions.ExtractionException; import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.info_list.ItemViewMode;
public final class ThemeHelper { public final class ThemeHelper {
private ThemeHelper() { private ThemeHelper() {
@ -332,7 +333,6 @@ public final class ThemeHelper {
} }
} }
/** /**
* Returns whether the grid layout or the list layout should be used. If the user set "auto" * Returns whether the grid layout or the list layout should be used. If the user set "auto"
* mode in settings, decides based on screen orientation (landscape) and size. * mode in settings, decides based on screen orientation (landscape) and size.
@ -341,19 +341,8 @@ public final class ThemeHelper {
* @return true:use grid layout, false:use list layout * @return true:use grid layout, false:use list layout
*/ */
public static boolean shouldUseGridLayout(final Context context) { public static boolean shouldUseGridLayout(final Context context) {
final String listMode = PreferenceManager.getDefaultSharedPreferences(context) final ItemViewMode mode = getItemViewMode(context);
.getString(context.getString(R.string.list_view_mode_key), return mode == ItemViewMode.GRID;
context.getString(R.string.list_view_mode_value));
if (listMode.equals(context.getString(R.string.list_view_mode_list_key))) {
return false;
} else if (listMode.equals(context.getString(R.string.list_view_mode_grid_key))) {
return true;
} else /* listMode.equals("auto") */ {
final Configuration configuration = context.getResources().getConfiguration();
return configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
&& configuration.isLayoutSizeAtLeast(Configuration.SCREENLAYOUT_SIZE_LARGE);
}
} }
/** /**
@ -367,6 +356,36 @@ public final class ThemeHelper {
context.getResources().getDimensionPixelSize(R.dimen.channel_item_grid_min_width)); context.getResources().getDimensionPixelSize(R.dimen.channel_item_grid_min_width));
} }
/**
* Returns item view mode.
* @param context to read preference and parse string
* @return Returns one of ItemViewMode
*/
public static ItemViewMode getItemViewMode(final Context context) {
final String listMode = PreferenceManager.getDefaultSharedPreferences(context)
.getString(context.getString(R.string.list_view_mode_key),
context.getString(R.string.list_view_mode_value));
final ItemViewMode result;
if (listMode.equals(context.getString(R.string.list_view_mode_list_key))) {
result = ItemViewMode.LIST;
} else if (listMode.equals(context.getString(R.string.list_view_mode_grid_key))) {
result = ItemViewMode.GRID;
} else if (listMode.equals(context.getString(R.string.list_view_mode_card_key))) {
result = ItemViewMode.CARD;
} else {
// Auto mode - evaluate whether to use Grid based on screen real estate.
final Configuration configuration = context.getResources().getConfiguration();
final boolean useGrid = configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
&& configuration.isLayoutSizeAtLeast(Configuration.SCREENLAYOUT_SIZE_LARGE);
if (useGrid) {
result = ItemViewMode.GRID;
} else {
result = ItemViewMode.LIST;
}
}
return result;
}
/** /**
* Calculates the number of grid stream info items that can fit horizontally on the screen. The * Calculates the number of grid stream info items that can fit horizontally on the screen. The
* width of a grid stream info item is obtained from the thumbnail width plus the right and left * width of a grid stream info item is obtained from the thumbnail width plus the right and left

View file

@ -0,0 +1,92 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/itemRoot"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:paddingTop="@dimen/margin_small"
android:paddingBottom="@dimen/spacing_micro"
android:clickable="true"
android:focusable="true">
<ImageView
android:id="@+id/itemThumbnailView"
android:layout_width="0dp"
android:layout_height="0dp"
android:scaleType="fitStart"
android:src="@drawable/placeholder_thumbnail_playlist"
app:layout_constraintDimensionRatio="16:9"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:ignore="RtlHardcoded" />
<View
android:id="@+id/videoCountOverlay"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="@color/playlist_stream_count_background_color"
app:layout_constraintBottom_toBottomOf="@id/itemThumbnailView"
app:layout_constraintEnd_toEndOf="@id/itemThumbnailView"
app:layout_constraintTop_toTopOf="@id/itemThumbnailView"
app:layout_constraintWidth_percent="0.35" />
<org.schabi.newpipe.views.NewPipeTextView
android:id="@+id/itemStreamCountView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="@color/duration_text_color"
android:textStyle="bold"
app:layout_constraintBottom_toTopOf="@id/playIcon"
app:layout_constraintEnd_toEndOf="@id/videoCountOverlay"
app:layout_constraintStart_toStartOf="@id/videoCountOverlay"
app:layout_constraintTop_toTopOf="@id/videoCountOverlay"
app:layout_constraintVertical_chainStyle="packed"
tools:text="314159" />
<!-- playIcon includes 8dp start margin to give center aligned look
when placed next to the video count -->
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/playIcon"
android:layout_width="@dimen/player_main_buttons_min_width"
android:layout_height="@dimen/player_main_buttons_min_width"
android:layout_marginStart="8dp"
android:src="@drawable/ic_playlist_play"
android:tint="@color/duration_text_color"
app:layout_constraintBottom_toBottomOf="@id/videoCountOverlay"
app:layout_constraintEnd_toEndOf="@id/videoCountOverlay"
app:layout_constraintStart_toStartOf="@id/videoCountOverlay"
app:layout_constraintTop_toBottomOf="@id/itemStreamCountView" />
<org.schabi.newpipe.views.NewPipeTextView
android:id="@+id/itemTitleView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/margin_small"
android:layout_marginTop="@dimen/spacing_nano"
android:layout_marginEnd="@dimen/margin_small"
android:ellipsize="end"
android:maxLines="2"
android:textAppearance="?android:textAppearanceListItem"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/itemThumbnailView"
tools:ignore="RtlHardcoded"
tools:text="Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc tristique vitae sem vitae blanditLorem ipsumLorem ipsumLorem ipsumLorem ipsumLorem ipsumLorem ipsumLorem ipsum" />
<org.schabi.newpipe.views.NewPipeTextView
android:id="@+id/itemUploaderView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:lines="1"
android:textAppearance="?android:textAppearanceSmall"
app:layout_constraintEnd_toEndOf="@id/itemTitleView"
app:layout_constraintStart_toStartOf="@id/itemTitleView"
app:layout_constraintTop_toBottomOf="@id/itemTitleView"
tools:ignore="RtlHardcoded"
tools:text="Uploader" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,99 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/itemRoot"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="@dimen/channel_item_grid_padding"
android:paddingBottom="@dimen/channel_item_grid_padding"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:focusable="true">
<ImageView
android:id="@+id/itemThumbnailView"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="2dp"
android:layout_marginEnd="2dp"
android:scaleType="centerCrop"
android:src="@drawable/placeholder_thumbnail_video"
app:layout_constraintBottom_toTopOf="@+id/itemProgressView"
app:layout_constraintDimensionRatio="16:9"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<org.schabi.newpipe.views.NewPipeTextView
android:id="@+id/itemDurationView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/video_item_search_duration_margin"
android:layout_marginBottom="@dimen/video_item_search_duration_margin"
android:background="@color/duration_background_color"
android:paddingHorizontal="@dimen/video_item_search_duration_horizontal_padding"
android:paddingVertical="@dimen/video_item_search_duration_vertical_padding"
android:textAllCaps="true"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="@color/duration_text_color"
android:textSize="@dimen/video_item_search_duration_text_size"
app:layout_constraintBottom_toBottomOf="@id/itemThumbnailView"
app:layout_constraintRight_toRightOf="@id/itemThumbnailView"
tools:text="1:09:10" />
<org.schabi.newpipe.views.NewPipeTextView
android:id="@+id/itemVideoTitleView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/margin_small"
android:layout_marginEnd="@dimen/margin_small"
android:ellipsize="end"
android:maxLines="2"
android:layout_marginTop="@dimen/margin_small"
android:textAppearance="?textAppearanceListItem"
app:layout_constraintEnd_toEndOf="@id/itemThumbnailView"
app:layout_constraintStart_toStartOf="@id/itemThumbnailView"
app:layout_constraintTop_toBottomOf="@id/itemThumbnailView"
tools:text="Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc tristique vitae sem vitae blanditLorem ipsumLorem ipsumLorem ipsumLorem ipsumLorem ipsumLorem ipsumLorem ipsum" />
<org.schabi.newpipe.views.NewPipeTextView
android:id="@+id/itemUploaderView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/spacing_micro"
android:layout_marginEnd="@dimen/margin_small"
android:ellipsize="end"
android:lines="1"
android:includeFontPadding="false"
android:textAppearance="?android:textAppearanceSmall"
app:layout_constraintEnd_toStartOf="@id/itemAdditionalDetails"
app:layout_constraintStart_toStartOf="@id/itemVideoTitleView"
app:layout_constraintTop_toBottomOf="@+id/itemVideoTitleView"
tools:text="Uploader" />
<org.schabi.newpipe.views.NewPipeTextView
android:id="@+id/itemAdditionalDetails"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="end"
android:lines="1"
android:includeFontPadding="false"
android:textAppearance="?android:textAppearanceSmall"
app:layout_constraintBottom_toBottomOf="@id/itemUploaderView"
app:layout_constraintEnd_toEndOf="@+id/itemVideoTitleView"
app:layout_constraintStart_toEndOf="@id/itemUploaderView"
app:layout_constraintTop_toTopOf="@id/itemUploaderView"
tools:text="2 years ago • 10M views" />
<org.schabi.newpipe.views.AnimatedProgressBar
android:id="@+id/itemProgressView"
style="@style/Widget.AppCompat.ProgressBar.Horizontal"
android:layout_width="0dp"
android:layout_height="4dp"
android:progressDrawable="?progress_horizontal_drawable"
app:layout_constraintBottom_toBottomOf="@id/itemThumbnailView"
app:layout_constraintEnd_toEndOf="@+id/itemThumbnailView"
app:layout_constraintStart_toStartOf="@+id/itemThumbnailView" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,96 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/itemRoot"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_normal"
android:layout_marginBottom="@dimen/margin_small"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:focusable="true">
<ImageView
android:id="@+id/itemThumbnailView"
android:layout_width="0dp"
android:layout_height="0dp"
android:scaleType="centerCrop"
android:src="@drawable/placeholder_thumbnail_video"
app:layout_constraintDimensionRatio="16:9"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:ignore="RtlHardcoded" />
<org.schabi.newpipe.views.NewPipeTextView
android:id="@+id/itemDurationView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/spacing_nano"
android:layout_marginBottom="@dimen/spacing_nano"
android:background="@color/duration_background_color"
android:paddingLeft="@dimen/video_item_search_duration_horizontal_padding"
android:paddingTop="@dimen/video_item_search_duration_vertical_padding"
android:paddingRight="@dimen/video_item_search_duration_horizontal_padding"
android:paddingBottom="@dimen/video_item_search_duration_vertical_padding"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="@color/duration_text_color"
android:textSize="@dimen/video_item_search_duration_text_size"
app:layout_constraintBottom_toBottomOf="@id/itemThumbnailView"
app:layout_constraintEnd_toEndOf="@id/itemThumbnailView"
tools:ignore="RtlHardcoded"
tools:text="1:09:10" />
<ImageView
android:id="@+id/itemHandle"
android:layout_width="48dp"
android:layout_height="36dp"
android:contentDescription="@string/detail_drag_description"
android:paddingLeft="@dimen/video_item_search_image_right_margin"
android:scaleType="center"
android:src="@drawable/ic_drag_handle"
app:layout_constraintEnd_toEndOf="@id/itemThumbnailView"
app:layout_constraintTop_toBottomOf="@id/itemThumbnailView"
tools:ignore="RtlHardcoded,RtlSymmetry" />
<org.schabi.newpipe.views.NewPipeTextView
android:id="@+id/itemVideoTitleView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_below="@id/itemThumbnailView"
android:layout_marginStart="@dimen/margin_small"
android:layout_marginTop="@dimen/spacing_nano"
android:layout_marginEnd="@dimen/spacing_micro"
android:ellipsize="end"
android:maxLines="2"
android:textAppearance="?android:textAppearanceListItem"
app:layout_constraintEnd_toStartOf="@id/itemHandle"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/itemThumbnailView"
tools:text="Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc tristique..." />
<org.schabi.newpipe.views.NewPipeTextView
android:id="@+id/itemAdditionalDetails"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_below="@+id/itemVideoTitleView"
android:layout_marginTop="@dimen/spacing_nano"
android:lines="1"
android:textAppearance="?android:attr/textAppearanceSmall"
app:layout_constraintEnd_toEndOf="@id/itemVideoTitleView"
app:layout_constraintStart_toStartOf="@id/itemVideoTitleView"
app:layout_constraintTop_toBottomOf="@id/itemVideoTitleView"
tools:text="Uploader" />
<org.schabi.newpipe.views.AnimatedProgressBar
android:id="@+id/itemProgressView"
style="@style/Widget.AppCompat.ProgressBar.Horizontal"
android:layout_width="wrap_content"
android:layout_height="4dp"
android:progressDrawable="?progress_horizontal_drawable"
app:layout_constraintBottom_toBottomOf="@id/itemThumbnailView"
app:layout_constraintEnd_toEndOf="@id/itemThumbnailView"
app:layout_constraintStart_toStartOf="@id/itemThumbnailView" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -3,4 +3,7 @@
(such as screen margins) for screens with more than 820dp of available width. This (such as screen margins) for screens with more than 820dp of available width. This
would include 7" and 10" devices in landscape (~960dp and ~1280dp respectively). --> would include 7" and 10" devices in landscape (~960dp and ~1280dp respectively). -->
<dimen name="activity_horizontal_margin">64dp</dimen> <dimen name="activity_horizontal_margin">64dp</dimen>
<dimen name="video_item_grid_thumbnail_image_width">280dp</dimen>
<dimen name="video_item_grid_thumbnail_image_height">160dp</dimen>
</resources> </resources>

View file

@ -1,5 +1,13 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<!-- Common dimensions -->
<dimen name="margin_normal">16dp</dimen>
<dimen name="margin_small">8dp</dimen>
<dimen name="margin_large">32dp</dimen>
<dimen name="spacing_normal">8dp</dimen>
<dimen name="spacing_micro">4dp</dimen>
<dimen name="spacing_nano">2dp</dimen>
<!-- Menu Drawer Dimensions --> <!-- Menu Drawer Dimensions -->
<dimen name="drawer_header_content_min_height">120dp</dimen> <dimen name="drawer_header_content_min_height">120dp</dimen>
<dimen name="drawer_header_padding_top">16dp</dimen> <dimen name="drawer_header_padding_top">16dp</dimen>

View file

@ -1263,16 +1263,19 @@
<string name="list_view_mode_auto_key">auto</string> <string name="list_view_mode_auto_key">auto</string>
<string name="list_view_mode_list_key">list</string> <string name="list_view_mode_list_key">list</string>
<string name="list_view_mode_grid_key">grid</string> <string name="list_view_mode_grid_key">grid</string>
<string name="list_view_mode_card_key">card</string>
<string-array name="list_view_mode_values"> <string-array name="list_view_mode_values">
<item>@string/list_view_mode_auto_key</item> <item>@string/list_view_mode_auto_key</item>
<item>@string/list_view_mode_list_key</item> <item>@string/list_view_mode_list_key</item>
<item>@string/list_view_mode_grid_key</item> <item>@string/list_view_mode_grid_key</item>
<item>@string/list_view_mode_card_key</item>
</string-array> </string-array>
<string-array name="list_view_mode_description"> <string-array name="list_view_mode_description">
<item>@string/auto</item> <item>@string/auto</item>
<item>@string/list</item> <item>@string/list</item>
<item>@string/grid</item> <item>@string/grid</item>
<item>@string/card</item>
</string-array> </string-array>
<string name="tablet_mode_key">tablet_mode</string> <string name="tablet_mode_key">tablet_mode</string>

View file

@ -550,6 +550,7 @@
<string name="list_view_mode">List view mode</string> <string name="list_view_mode">List view mode</string>
<string name="list">List</string> <string name="list">List</string>
<string name="grid">Grid</string> <string name="grid">Grid</string>
<string name="card">Card</string>
<string name="auto">Auto</string> <string name="auto">Auto</string>
<!-- Seekbar Preview Thumbnail--> <!-- Seekbar Preview Thumbnail-->
<string name="seekbar_preview_thumbnail_title">Seekbar thumbnail preview</string> <string name="seekbar_preview_thumbnail_title">Seekbar thumbnail preview</string>